Repository: sindresorhus/got (ts)

Issue:
Got throws an error instead of redirecting for 303, 307 or 308
```javascript
{ HTTPError: Response code 308 (Permanent Redirect)
    at EventEmitter.ee.on.res (/home/user/Desktop/REPOS/transfer-sh/node_modules/got/index.js:182:24)
    at emitOne (events.js:96:13)
    at EventEmitter.emit (events.js:188:7)
    at Immediate.setImmediate (/home/user/Desktop/REPOS/transfer-sh/node_modules/got/index.js:61:8)
    at tryOnImmediate (timers.js:543:15)
    at processImmediate [as _immediateCallback] (timers.js:523:5)
  message: 'Response code 308 (Permanent Redirect)',
  host: 'transfer.sh',
  hostname: 'transfer.sh',
  method: 'PUT',
  path: '/README.md',
  statusCode: 308,
  statusMessage: 'Permanent Redirect' }
```

Redirect not followed?

Pull request:
Redirect on 307, 308, 303
Fixes #291.

Open questions:
- Should there be an option for whether GET or HEAD are used to follow up a 303?
- I have written this for clarity over performance. LMK if you'd prefer it be more performance-oriented.

TODO:
- [x] Update README

Patch:
diff --git a/index.js b/index.js
index 160b0ff..014fcc2 100644
--- a/index.js
+++ b/index.js
@@ -11,9 +11,7 @@ const getStream = require('get-stream');
 const timedOut = require('timed-out');
 const urlParseLax = require('url-parse-lax');
 const lowercaseKeys = require('lowercase-keys');
-const isRedirect = require('is-redirect');
 const decompressResponse = require('decompress-response');
-const createErrorClass = require('create-error-class');
 const isRetryAllowed = require('is-retry-allowed');
 const Buffer = require('safe-buffer').Buffer;
 const isURL = require('isurl');
@@ -21,6 +19,9 @@ const isPlainObj = require('is-plain-obj');
 const PCancelable = require('p-cancelable');
 const pkg = require('./package');
 
+const getMethodRedirectCodes = new Set([300, 301, 302, 303, 304, 305, 307, 308]);
+const allMethodRedirectCodes = new Set([300, 303, 307, 308]);
+
 function requestAsEventEmitter(opts) {
 	opts = opts || {};
 
@@ -36,7 +37,11 @@ function requestAsEventEmitter(opts) {
 			return;
 		}
 
-		const fn = opts.protocol === 'https:' ? https : http;
+		let fn = opts.protocol === 'https:' ? https : http;
+
+		if (process.versions.electron) {
+			fn = require('electron').net;
+		}
 
 		const req = fn.request(opts, res => {
 			const statusCode = res.statusCode;
@@ -44,9 +49,19 @@ function requestAsEventEmitter(opts) {
 			res.url = redirectUrl || requestUrl;
 			res.requestUrl = requestUrl;
 
-			if (isRedirect(statusCode) && opts.followRedirect && 'location' in res.headers && (opts.method === 'GET' || opts.method === 'HEAD')) {
+			const followRedirect = opts.followRedirect && 'location' in res.headers;
+			const redirectGet = followRedirect && getMethodRedirectCodes.has(statusCode);
+			const redirectAll = followRedirect && allMethodRedirectCodes.has(statusCode);
+
+			if (redirectAll || (redirectGet && (opts.method === 'GET' || opts.method === 'HEAD'))) {
 				res.resume();
 
+				if (statusCode === 303) {
+					// Server responded with "see other", indicating that the resource exists at another location,
+					// and the client should request it from that location via GET or HEAD.
+					opts.method = 'GET';
+				}
+
 				if (redirects.length >= 10) {
 					ee.emit('error', new got.MaxRedirectsError(statusCode, redirects, opts), null, res);
 					return;
@@ -97,7 +112,9 @@ function requestAsEventEmitter(opts) {
 		});
 	};
 
-	get(opts);
+	setImmediate(() => {
+		get(opts);
+	});
 	return ee;
 }
 
@@ -371,50 +388,75 @@ for (const el of helpers) {
 	got.stream[el] = (url, opts) => got.stream(url, Object.assign({}, opts, {method: el}));
 }
 
-function stdError(error, opts) {
-	if (error.code !== undefined) {
-		this.code = error.code;
-	}
+class StdError extends Error {
+	constructor(message, error, opts) {
+		super(message);
+		this.name = 'StdError';
 
-	Object.assign(this, {
-		message: error.message,
-		host: opts.host,
-		hostname: opts.hostname,
-		method: opts.method,
-		path: opts.path,
-		protocol: opts.protocol,
-		url: opts.href
-	});
-}
+		if (error.code !== undefined) {
+			this.code = error.code;
+		}
 
-got.RequestError = createErrorClass('RequestError', stdError);
-got.ReadError = createErrorClass('ReadError', stdError);
-got.ParseError = createErrorClass('ParseError', function (e, statusCode, opts, data) {
-	stdError.call(this, e, opts);
-	this.statusCode = statusCode;
-	this.statusMessage = http.STATUS_CODES[this.statusCode];
-	this.message = `${e.message} in "${urlLib.format(opts)}": \n${data.slice(0, 77)}...`;
-});
+		Object.assign(this, {
+			host: opts.host,
+			hostname: opts.hostname,
+			method: opts.method,
+			path: opts.path,
+			protocol: opts.protocol,
+			url: opts.href
+		});
+	}
+}
 
-got.HTTPError = createErrorClass('HTTPError', function (statusCode, headers, opts) {
-	stdError.call(this, {}, opts);
-	this.statusCode = statusCode;
-	this.statusMessage = http.STATUS_CODES[this.statusCode];
-	this.message = `Response code ${this.statusCode} (${this.statusMessage})`;
-	this.headers = headers;
-});
+got.RequestError = class extends StdError {
+	constructor(error, opts) {
+		super(error.message, error, opts);
+		this.name = 'RequestError';
+	}
+};
 
-got.MaxRedirectsError = createErrorClass('MaxRedirectsError', function (statusCode, redirectUrls, opts) {
-	stdError.call(this, {}, opts);
-	this.statusCode = statusCode;
-	this.statusMessage = http.STATUS_CODES[this.statusCode];
-	this.message = 'Redirected 10 times. Aborting.';
-	this.redirectUrls = redirectUrls;
-});
+got.ReadError = class extends StdError {
+	constructor(error, opts) {
+		super(error.message, error, opts);
+		this.name = 'ReadError';
+	}
+};
+
+got.ParseError = class extends StdError {
+	constructor(error, statusCode, opts, data) {
+		super(`${error.message} in "${urlLib.format(opts)}": \n${data.slice(0, 77)}...`, error, opts);
+		this.name = 'ParseError';
+		this.statusCode = statusCode;
+		this.statusMessage = http.STATUS_CODES[this.statusCode];
+	}
+};
+
+got.HTTPError = class extends StdError {
+	constructor(statusCode, headers, opts) {
+		const statusMessage = http.STATUS_CODES[statusCode];
+		super(`Response code ${statusCode} (${statusMessage})`, {}, opts);
+		this.name = 'HTTPError';
+		this.statusCode = statusCode;
+		this.statusMessage = statusMessage;
+		this.headers = headers;
+	}
+};
+
+got.MaxRedirectsError = class extends StdError {
+	constructor(statusCode, redirectUrls, opts) {
+		super('Redirected 10 times. Aborting.', {}, opts);
+		this.name = 'MaxRedirectsError';
+		this.statusCode = statusCode;
+		this.statusMessage = http.STATUS_CODES[this.statusCode];
+		this.redirectUrls = redirectUrls;
+	}
+};
 
-got.UnsupportedProtocolError = createErrorClass('UnsupportedProtocolError', function (opts) {
-	stdError.call(this, {}, opts);
-	this.message = `Unsupported protocol "${opts.protocol}"`;
-});
+got.UnsupportedProtocolError = class extends StdError {
+	constructor(opts) {
+		super(`Unsupported protocol "${opts.protocol}"`, {}, opts);
+		this.name = 'UnsupportedProtocolError';
+	}
+};
 
 module.exports = got;
diff --git a/package.json b/package.json
index 48beeb6..9df30b0 100644
--- a/package.json
+++ b/package.json
@@ -44,15 +44,16 @@
     "simple",
     "curl",
     "wget",
-    "fetch"
+    "fetch",
+    "net",
+    "network",
+    "electron"
   ],
   "dependencies": {
-    "create-error-class": "^3.0.0",
     "decompress-response": "^3.2.0",
     "duplexer3": "^0.1.4",
     "get-stream": "^3.0.0",
     "is-plain-obj": "^1.1.0",
-    "is-redirect": "^1.0.0",
     "is-retry-allowed": "^1.0.0",
     "is-stream": "^1.0.0",
     "isurl": "^1.0.0-alpha5",
diff --git a/readme.md b/readme.md
index 6f25119..dcb91e4 100644
--- a/readme.md
+++ b/readme.md
@@ -16,6 +16,8 @@ It supports following redirects, promises, streams, retries, automagically handl
 
 Created because [`request`](https://github.com/request/request) is bloated *(several megabytes!)*.
 
+When used with Electron, it takes advantage of [`electron.net`](https://electron.atom.io/docs/api/net/).
+
 
 ## Install
 
@@ -147,6 +149,8 @@ Default: `true`
 
 Defines if redirect responses should be followed automatically.
 
+Note that if a `303` is sent by the server in response to any request type (`POST`, `DELETE`, etc.), got will automatically
+request the resource pointed to in the location header via `GET`. This is in accordance with [the spec](https://tools.ietf.org/html/rfc7231#section-6.4.4).
 
 #### Streams
 


Test patch:
diff --git a/test/redirects.js b/test/redirects.js
index 2b1a93f..991e75d 100644
--- a/test/redirects.js
+++ b/test/redirects.js
@@ -78,6 +78,27 @@ test.before('setup', async () => {
 		res.end();
 	});
 
+	http.on('/seeOther', (req, res) => {
+		res.writeHead(303, {
+			location: '/'
+		});
+		res.end();
+	});
+
+	http.on('/temporary', (req, res) => {
+		res.writeHead(307, {
+			location: '/'
+		});
+		res.end();
+	});
+
+	http.on('/permanent', (req, res) => {
+		res.writeHead(308, {
+			location: '/'
+		});
+		res.end();
+	});
+
 	http.on('/relativeQuery?bang', (req, res) => {
 		res.writeHead(302, {
 			location: '/'
@@ -97,9 +118,17 @@ test.before('setup', async () => {
 });
 
 test('follows redirect', async t => {
-	const response = await got(`${http.url}/finite`);
-	t.is(response.body, 'reached');
-	t.deepEqual(response.redirectUrls, [`${http.url}/`]);
+	const {body, redirectUrls} = await got(`${http.url}/finite`);
+	t.is(body, 'reached');
+	t.deepEqual(redirectUrls, [`${http.url}/`]);
+});
+
+test('follows 307, 308 redirect', async t => {
+	const tempBody = (await got(`${http.url}/temporary`)).body;
+	t.is(tempBody, 'reached');
+
+	const permBody = (await got(`${http.url}/permanent`)).body;
+	t.is(permBody, 'reached');
 });
 
 test('does not follow redirect when disabled', async t => {
@@ -142,6 +171,12 @@ test('redirect only GET and HEAD requests', async t => {
 	}
 });
 
+test('redirect on 303 response even with post, put, delete', async t => {
+	const {url, body} = await got(`${http.url}/seeOther`, {body: 'wow'});
+	t.is(url, `${http.url}/`);
+	t.is(body, 'reached');
+});
+
 test('redirects from http to https works', async t => {
 	t.truthy((await got(`${http.url}/httpToHttps`, {rejectUnauthorized: false})).body);
 });