Skip to content

Commit 7c3f147

Browse files
Make followRedirect option accept a function (#2306)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 844cfb6 commit 7c3f147

File tree

5 files changed

+111
-69
lines changed

5 files changed

+111
-69
lines changed

documentation/2-options.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -681,10 +681,12 @@ Only useful when the `cookieJar` option has been set.
681681
682682
### `followRedirect`
683683

684-
**Type: `boolean`**\
684+
**Type: `boolean | (response: PlainResponse) => boolean`**\
685685
**Default: `true`**
686686

687-
Defines if redirect responses should be followed automatically.
687+
Whether redirect responses should be followed automatically.
688+
689+
Optionally, pass a function to dynamically decide based on the response object.
688690

689691
#### **Note:**
690692
> - If a `303` is sent by the server in response to any request type (POST, DELETE, etc.), Got will request the resource pointed to in the location header via GET.\

source/core/index.ts

+65-62
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {
336336
void (async () => {
337337
// Node.js parser is really weird.
338338
// It emits post-request Parse Errors on the same instance as previous request. WTF.
339-
// Therefore we need to check if it has been destroyed as well.
339+
// Therefore, we need to check if it has been destroyed as well.
340340
//
341341
// Furthermore, Node.js 16 `response.destroy()` doesn't immediately destroy the socket,
342342
// but makes the response unreadable. So we additionally need to check `response.readable`.
@@ -723,95 +723,98 @@ export default class Request extends Duplex implements RequestEvents<Request> {
723723
return;
724724
}
725725

726-
if (options.followRedirect && response.headers.location && redirectCodes.has(statusCode)) {
726+
if (response.headers.location && redirectCodes.has(statusCode)) {
727727
// We're being redirected, we don't care about the response.
728728
// It'd be best to abort the request, but we can't because
729729
// we would have to sacrifice the TCP connection. We don't want that.
730-
response.resume();
731-
732-
this._cancelTimeouts();
733-
this._unproxyEvents();
734-
735-
if (this.redirectUrls.length >= options.maxRedirects) {
736-
this._beforeError(new MaxRedirectsError(this));
737-
return;
738-
}
730+
const shouldFollow = typeof options.followRedirect === 'function' ? options.followRedirect(typedResponse) : options.followRedirect;
731+
if (shouldFollow) {
732+
response.resume();
739733

740-
this._request = undefined;
734+
this._cancelTimeouts();
735+
this._unproxyEvents();
741736

742-
const updatedOptions = new Options(undefined, undefined, this.options);
737+
if (this.redirectUrls.length >= options.maxRedirects) {
738+
this._beforeError(new MaxRedirectsError(this));
739+
return;
740+
}
743741

744-
const serverRequestedGet = statusCode === 303 && updatedOptions.method !== 'GET' && updatedOptions.method !== 'HEAD';
745-
const canRewrite = statusCode !== 307 && statusCode !== 308;
746-
const userRequestedGet = updatedOptions.methodRewriting && canRewrite;
742+
this._request = undefined;
747743

748-
if (serverRequestedGet || userRequestedGet) {
749-
updatedOptions.method = 'GET';
744+
const updatedOptions = new Options(undefined, undefined, this.options);
750745

751-
updatedOptions.body = undefined;
752-
updatedOptions.json = undefined;
753-
updatedOptions.form = undefined;
746+
const serverRequestedGet = statusCode === 303 && updatedOptions.method !== 'GET' && updatedOptions.method !== 'HEAD';
747+
const canRewrite = statusCode !== 307 && statusCode !== 308;
748+
const userRequestedGet = updatedOptions.methodRewriting && canRewrite;
754749

755-
delete updatedOptions.headers['content-length'];
756-
}
750+
if (serverRequestedGet || userRequestedGet) {
751+
updatedOptions.method = 'GET';
757752

758-
try {
759-
// We need this in order to support UTF-8
760-
const redirectBuffer = Buffer.from(response.headers.location, 'binary').toString();
761-
const redirectUrl = new URL(redirectBuffer, url);
753+
updatedOptions.body = undefined;
754+
updatedOptions.json = undefined;
755+
updatedOptions.form = undefined;
762756

763-
if (!isUnixSocketURL(url as URL) && isUnixSocketURL(redirectUrl)) {
764-
this._beforeError(new RequestError('Cannot redirect to UNIX socket', {}, this));
765-
return;
757+
delete updatedOptions.headers['content-length'];
766758
}
767759

768-
// Redirecting to a different site, clear sensitive data.
769-
if (redirectUrl.hostname !== (url as URL).hostname || redirectUrl.port !== (url as URL).port) {
770-
if ('host' in updatedOptions.headers) {
771-
delete updatedOptions.headers.host;
772-
}
760+
try {
761+
// We need this in order to support UTF-8
762+
const redirectBuffer = Buffer.from(response.headers.location, 'binary').toString();
763+
const redirectUrl = new URL(redirectBuffer, url);
773764

774-
if ('cookie' in updatedOptions.headers) {
775-
delete updatedOptions.headers.cookie;
765+
if (!isUnixSocketURL(url as URL) && isUnixSocketURL(redirectUrl)) {
766+
this._beforeError(new RequestError('Cannot redirect to UNIX socket', {}, this));
767+
return;
776768
}
777769

778-
if ('authorization' in updatedOptions.headers) {
779-
delete updatedOptions.headers.authorization;
780-
}
770+
// Redirecting to a different site, clear sensitive data.
771+
if (redirectUrl.hostname !== (url as URL).hostname || redirectUrl.port !== (url as URL).port) {
772+
if ('host' in updatedOptions.headers) {
773+
delete updatedOptions.headers.host;
774+
}
775+
776+
if ('cookie' in updatedOptions.headers) {
777+
delete updatedOptions.headers.cookie;
778+
}
781779

782-
if (updatedOptions.username || updatedOptions.password) {
783-
updatedOptions.username = '';
784-
updatedOptions.password = '';
780+
if ('authorization' in updatedOptions.headers) {
781+
delete updatedOptions.headers.authorization;
782+
}
783+
784+
if (updatedOptions.username || updatedOptions.password) {
785+
updatedOptions.username = '';
786+
updatedOptions.password = '';
787+
}
788+
} else {
789+
redirectUrl.username = updatedOptions.username;
790+
redirectUrl.password = updatedOptions.password;
785791
}
786-
} else {
787-
redirectUrl.username = updatedOptions.username;
788-
redirectUrl.password = updatedOptions.password;
789-
}
790792

791-
this.redirectUrls.push(redirectUrl);
792-
updatedOptions.prefixUrl = '';
793-
updatedOptions.url = redirectUrl;
793+
this.redirectUrls.push(redirectUrl);
794+
updatedOptions.prefixUrl = '';
795+
updatedOptions.url = redirectUrl;
794796

795-
for (const hook of updatedOptions.hooks.beforeRedirect) {
796-
// eslint-disable-next-line no-await-in-loop
797-
await hook(updatedOptions, typedResponse);
798-
}
797+
for (const hook of updatedOptions.hooks.beforeRedirect) {
798+
// eslint-disable-next-line no-await-in-loop
799+
await hook(updatedOptions, typedResponse);
800+
}
799801

800-
this.emit('redirect', updatedOptions, typedResponse);
802+
this.emit('redirect', updatedOptions, typedResponse);
801803

802-
this.options = updatedOptions;
804+
this.options = updatedOptions;
805+
806+
await this._makeRequest();
807+
} catch (error: any) {
808+
this._beforeError(error);
809+
return;
810+
}
803811

804-
await this._makeRequest();
805-
} catch (error: any) {
806-
this._beforeError(error);
807812
return;
808813
}
809-
810-
return;
811814
}
812815

813816
// `HTTPError`s always have `error.response.body` defined.
814-
// Therefore we cannot retry if `options.throwHttpErrors` is false.
817+
// Therefore, we cannot retry if `options.throwHttpErrors` is false.
815818
// On the last retry, if `options.throwHttpErrors` is false, we would need to return the body,
816819
// but that wouldn't be possible since the body would be already read in `error.response.body`.
817820
if (options.isStream && options.throwHttpErrors && !isResponseOk(typedResponse)) {

source/core/options.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -1756,19 +1756,21 @@ export default class Options {
17561756
}
17571757

17581758
/**
1759-
Defines if redirect responses should be followed automatically.
1759+
Whether redirect responses should be followed automatically.
1760+
1761+
Optionally, pass a function to dynamically decide based on the response object.
17601762
17611763
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`.
17621764
This is in accordance with [the spec](https://tools.ietf.org/html/rfc7231#section-6.4.4). You can optionally turn on this behavior also for other redirect codes - see `methodRewriting`.
17631765
17641766
@default true
17651767
*/
1766-
get followRedirect(): boolean {
1768+
get followRedirect(): boolean | ((response: PlainResponse) => boolean) {
17671769
return this._internals.followRedirect;
17681770
}
17691771

1770-
set followRedirect(value: boolean) {
1771-
assert.boolean(value);
1772+
set followRedirect(value: boolean | ((response: PlainResponse) => boolean)) {
1773+
assert.any([is.boolean, is.function_], value);
17721774

17731775
this._internals.followRedirect = value;
17741776
}

source/core/response.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,9 @@ export type Response<T = unknown> = {
114114

115115
export const isResponseOk = (response: PlainResponse): boolean => {
116116
const {statusCode} = response;
117-
const limitStatusCode = response.request.options.followRedirect ? 299 : 399;
117+
const {followRedirect} = response.request.options;
118+
const shouldFollow = typeof followRedirect === 'function' ? followRedirect(response) : followRedirect;
119+
const limitStatusCode = shouldFollow ? 299 : 399;
118120

119121
return (statusCode >= 200 && statusCode <= limitStatusCode) || statusCode === 304;
120122
};

test/redirects.ts

+33
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,39 @@ test('follows redirect', withServer, async (t, server, got) => {
8989
t.deepEqual(redirectUrls.map(String), [`${server.url}/`]);
9090
});
9191

92+
test('does not follow redirect when followRedirect is a function and returns false', withServer, async (t, server, got) => {
93+
server.get('/', reachedHandler);
94+
server.get('/finite', finiteHandler);
95+
96+
const {body, statusCode} = await got('finite', {followRedirect: () => false});
97+
t.not(body, 'reached');
98+
t.is(statusCode, 302);
99+
});
100+
101+
test('follows redirect when followRedirect is a function and returns true', withServer, async (t, server, got) => {
102+
server.get('/', reachedHandler);
103+
server.get('/finite', finiteHandler);
104+
105+
const {body, redirectUrls} = await got('finite', {followRedirect: () => true});
106+
t.is(body, 'reached');
107+
t.deepEqual(redirectUrls.map(String), [`${server.url}/`]);
108+
});
109+
110+
test('followRedirect gets plainResponse and does not follow', withServer, async (t, server, got) => {
111+
server.get('/temporary', (_request, response) => {
112+
response.writeHead(307, {
113+
location: '/redirect',
114+
});
115+
response.end();
116+
});
117+
118+
const {statusCode} = await got('temporary', {followRedirect(response) {
119+
t.is(response.headers.location, '/redirect');
120+
return false;
121+
}});
122+
t.is(statusCode, 307);
123+
});
124+
92125
test('follows 307, 308 redirect', withServer, async (t, server, got) => {
93126
server.get('/', reachedHandler);
94127

0 commit comments

Comments
 (0)