Repository: sindresorhus/got (ts)

Issue:
Get intermediate redirect urls
It would be useful if there were a way to get all redirected urls in a request. So if you request url `a`, but it redirects to `b` which redirects to `c` and finally to `d`, if there were a `urls` property on the response that would return `['a', 'b', 'c', 'd']`. There's the `redirect` event that could be used to capture these urls but that seems to only work when using streams.


Pull request:
Include response headers and redirect URLs in error object
Fixes #234.

- Adds a response headers object to HTTPError
- Adds an array of redirects that were followed to the response object and MaxRedirectsError

Patch:
diff --git a/index.js b/index.js
index 41f71de..7691716 100644
--- a/index.js
+++ b/index.js
@@ -24,7 +24,7 @@ function requestAsEventEmitter(opts) {
 
 	const ee = new EventEmitter();
 	const requestUrl = opts.href || urlLib.resolve(urlLib.format(opts), opts.path);
-	let redirectCount = 0;
+	const redirects = [];
 	let retryCount = 0;
 	let redirectUrl;
 
@@ -45,14 +45,17 @@ function requestAsEventEmitter(opts) {
 			if (isRedirect(statusCode) && opts.followRedirect && 'location' in res.headers && (opts.method === 'GET' || opts.method === 'HEAD')) {
 				res.resume();
 
-				if (++redirectCount > 10) {
-					ee.emit('error', new got.MaxRedirectsError(statusCode, opts), null, res);
+				if (redirects.length >= 10) {
+					ee.emit('error', new got.MaxRedirectsError(statusCode, redirects, opts), null, res);
 					return;
 				}
 
 				const bufferString = Buffer.from(res.headers.location, 'binary').toString();
 
 				redirectUrl = urlLib.resolve(urlLib.format(opts), bufferString);
+
+				redirects.push(redirectUrl);
+
 				const redirectOpts = Object.assign({}, opts, urlLib.parse(redirectUrl));
 
 				ee.emit('redirect', res, redirectOpts);
@@ -65,6 +68,9 @@ function requestAsEventEmitter(opts) {
 			setImmediate(() => {
 				const response = typeof decompressResponse === 'function' &&
 					req.method !== 'HEAD' ? decompressResponse(res) : res;
+
+				response.redirectUrls = redirects;
+
 				ee.emit('response', response);
 			});
 		});
@@ -127,7 +133,7 @@ function asPromise(opts) {
 					}
 
 					if (statusCode !== 304 && (statusCode < 200 || statusCode > limitStatusCode)) {
-						throw new got.HTTPError(statusCode, opts);
+						throw new got.HTTPError(statusCode, res.headers, opts);
 					}
 
 					resolve(res);
@@ -186,7 +192,7 @@ function asStream(opts) {
 		res.pipe(output);
 
 		if (statusCode !== 304 && (statusCode < 200 || statusCode > 299)) {
-			proxy.emit('error', new got.HTTPError(statusCode, opts), null, res);
+			proxy.emit('error', new got.HTTPError(statusCode, res.headers, opts), null, res);
 			return;
 		}
 
@@ -365,18 +371,20 @@ got.ParseError = createErrorClass('ParseError', function (e, statusCode, opts, d
 	this.message = `${e.message} in "${urlLib.format(opts)}": \n${data.slice(0, 77)}...`;
 });
 
-got.HTTPError = createErrorClass('HTTPError', function (statusCode, opts) {
+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.MaxRedirectsError = createErrorClass('MaxRedirectsError', function (statusCode, opts) {
+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.UnsupportedProtocolError = createErrorClass('UnsupportedProtocolError', function (opts) {
diff --git a/readme.md b/readme.md
index 7ea3ea5..9d2579c 100644
--- a/readme.md
+++ b/readme.md
@@ -207,11 +207,11 @@ When `json` option is enabled and `JSON.parse` fails.
 
 #### got.HTTPError
 
-When server response code is not 2xx. Contains `statusCode` and `statusMessage`.
+When server response code is not 2xx. Includes `statusCode`, `statusMessage`, and `redirectUrls` properties.
 
 #### got.MaxRedirectsError
 
-When server redirects you more than 10 times.
+When server redirects you more than 10 times. Includes a `redirectUrls` property, which is an array of the URLs Got was redirected to before giving up.
 
 #### got.UnsupportedProtocolError
 


Test patch:
diff --git a/test/error.js b/test/error.js
index ff0f385..7e5efd8 100644
--- a/test/error.js
+++ b/test/error.js
@@ -29,6 +29,7 @@ test('properties', async t => {
 		t.is(err.method, 'GET');
 		t.is(err.protocol, 'http:');
 		t.is(err.url, err.response.requestUrl);
+		t.is(err.headers.connection, 'close');
 	}
 });
 
diff --git a/test/redirects.js b/test/redirects.js
index 2ec75d0..5a7a781 100644
--- a/test/redirects.js
+++ b/test/redirects.js
@@ -97,7 +97,9 @@ test.before('setup', async () => {
 });
 
 test('follows redirect', async t => {
-	t.is((await got(`${http.url}/finite`)).body, 'reached');
+	const response = await got(`${http.url}/finite`);
+	t.is(response.body, 'reached');
+	t.deepEqual(response.redirectUrls, [`${http.url}/`]);
 });
 
 test('does not follow redirect when disabled', async t => {
@@ -114,6 +116,7 @@ test('throws on endless redirect', async t => {
 		t.fail('Exception was not thrown');
 	} catch (err) {
 		t.is(err.message, 'Redirected 10 times. Aborting.');
+		t.deepEqual(err.redirectUrls, Array(10).fill(`${http.url}/endless`));
 	}
 });