Skip to content

Commit b7ead5f

Browse files
authoredNov 5, 2019
Allow method rewriting on redirects (#913)
1 parent e09dfcd commit b7ead5f

File tree

4 files changed

+174
-47
lines changed

4 files changed

+174
-47
lines changed
 

‎readme.md

+2
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,8 @@ Defines if redirect responses should be followed automatically.
368368

369369
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).
370370

371+
This supports [method rewriting](https://tools.ietf.org/html/rfc7231#section-6.4). For example, when sending a POST request and receiving a `302`, it will resend that request to the new location.
372+
371373
###### maxRedirects
372374

373375
Type: `number`<br>

‎source/request-as-event-emitter.ts

+37-38
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,8 @@ import urlToOptions from './utils/url-to-options';
2020
import {RequestFunction, NormalizedOptions, Response, ResponseObject, AgentByProtocol} from './utils/types';
2121
import dynamicRequire from './utils/dynamic-require';
2222

23-
export type GetMethodRedirectCodes = 300 | 301 | 302 | 303 | 304 | 305 | 307 | 308;
24-
export type AllMethodRedirectCodes = 300 | 303 | 307 | 308;
25-
export type WithoutBody = 'GET' | 'HEAD';
26-
27-
const getMethodRedirectCodes: ReadonlySet<GetMethodRedirectCodes> = new Set([300, 301, 302, 303, 304, 305, 307, 308]);
28-
const allMethodRedirectCodes: ReadonlySet<AllMethodRedirectCodes> = new Set([300, 303, 307, 308]);
29-
const withoutBody: ReadonlySet<WithoutBody> = new Set(['GET', 'HEAD']);
23+
const redirectCodes: ReadonlySet<number> = new Set([300, 301, 302, 303, 304, 307, 308]);
24+
const withoutBody: ReadonlySet<string> = new Set(['GET', 'HEAD']);
3025

3126
export interface RequestAsEventEmitter extends EventEmitter {
3227
retry: <T extends Error>(error: T) => boolean;
@@ -143,46 +138,50 @@ export default (options: NormalizedOptions, input?: TransformStream) => {
143138
await Promise.all(promises);
144139
}
145140

146-
if (options.followRedirect && 'location' in typedResponse.headers) {
147-
if (allMethodRedirectCodes.has(statusCode as AllMethodRedirectCodes) || (getMethodRedirectCodes.has(statusCode as GetMethodRedirectCodes) && (options.method === 'GET' || options.method === 'HEAD'))) {
148-
typedResponse.resume(); // We're being redirected, we don't care about the response.
149-
150-
if (statusCode === 303) {
151-
// Server responded with "see other", indicating that the resource exists at another location,
152-
// and the client should request it from that location via GET or HEAD.
153-
options.method = 'GET';
154-
}
141+
if (options.followRedirect && Reflect.has(typedResponse.headers, 'location') && redirectCodes.has(statusCode)) {
142+
typedResponse.resume(); // We're being redirected, we don't care about the response.
155143

156-
if (redirects.length >= options.maxRedirects) {
157-
throw new MaxRedirectsError(typedResponse, options.maxRedirects, options);
158-
}
144+
if (statusCode === 303 && options.method !== 'GET' && options.method !== 'HEAD') {
145+
// Server responded with "see other", indicating that the resource exists at another location,
146+
// and the client should request it from that location via GET or HEAD.
147+
options.method = 'GET';
159148

160-
// Handles invalid URLs. See https://github.com/sindresorhus/got/issues/604
161-
const redirectBuffer = Buffer.from(typedResponse.headers.location, 'binary').toString();
162-
const redirectURL = new URL(redirectBuffer, currentUrl);
163-
redirectString = redirectURL.toString();
149+
delete options.json;
150+
delete options.form;
151+
delete options.body;
152+
}
164153

165-
redirects.push(redirectString);
154+
if (redirects.length >= options.maxRedirects) {
155+
throw new MaxRedirectsError(typedResponse, options.maxRedirects, options);
156+
}
166157

167-
const redirectOptions = {
168-
...options,
169-
port: undefined,
170-
auth: undefined,
171-
...urlToOptions(redirectURL)
172-
};
158+
// Handles invalid URLs. See https://github.com/sindresorhus/got/issues/604
159+
const redirectBuffer = Buffer.from(typedResponse.headers.location, 'binary').toString();
160+
const redirectURL = new URL(redirectBuffer, currentUrl);
161+
redirectString = redirectURL.toString();
173162

174-
for (const hook of options.hooks.beforeRedirect) {
175-
// eslint-disable-next-line no-await-in-loop
176-
await hook(redirectOptions, typedResponse);
177-
}
163+
redirects.push(redirectString);
178164

179-
emitter.emit('redirect', response, redirectOptions);
165+
const redirectOptions = {
166+
...options,
167+
port: undefined,
168+
auth: undefined,
169+
...urlToOptions(redirectURL)
170+
};
180171

181-
await get(redirectOptions);
182-
return;
172+
for (const hook of options.hooks.beforeRedirect) {
173+
// eslint-disable-next-line no-await-in-loop
174+
await hook(redirectOptions, typedResponse);
183175
}
176+
177+
emitter.emit('redirect', response, redirectOptions);
178+
179+
await get(redirectOptions);
180+
return;
184181
}
185182

183+
delete options.body;
184+
186185
getResponse(typedResponse, options, emitter);
187186
} catch (error) {
188187
emitError(error);
@@ -362,7 +361,7 @@ export default (options: NormalizedOptions, input?: TransformStream) => {
362361
const isForm = !is.nullOrUndefined(options.form);
363362
const isJSON = !is.nullOrUndefined(options.json);
364363
const isBody = !is.nullOrUndefined(body);
365-
if ((isBody || isForm || isJSON) && withoutBody.has(options.method as WithoutBody)) {
364+
if ((isBody || isForm || isJSON) && withoutBody.has(options.method)) {
366365
throw new TypeError(`The \`${options.method}\` method cannot be used with a body`);
367366
}
368367

‎test/arguments.ts

+64
Original file line numberDiff line numberDiff line change
@@ -313,3 +313,67 @@ test('`context` option is accessible when extending instances', t => {
313313
t.is(instance.defaults.options.context, context);
314314
t.false({}.propertyIsEnumerable.call(instance.defaults.options, 'context'));
315315
});
316+
317+
test('`options.body` is cleaned up when retrying - `options.json`', withServer, async (t, server, got) => {
318+
let first = true;
319+
server.post('/', (_request, response) => {
320+
if (first) {
321+
first = false;
322+
323+
response.statusCode = 401;
324+
}
325+
326+
response.end();
327+
});
328+
329+
await t.notThrowsAsync(got.post('', {
330+
hooks: {
331+
afterResponse: [
332+
async (response, retryWithMergedOptions) => {
333+
if (response.statusCode === 401) {
334+
return retryWithMergedOptions();
335+
}
336+
337+
t.is(response.request.options.body, undefined);
338+
339+
return response;
340+
}
341+
]
342+
},
343+
json: {
344+
some: 'data'
345+
}
346+
}));
347+
});
348+
349+
test('`options.body` is cleaned up when retrying - `options.form`', withServer, async (t, server, got) => {
350+
let first = true;
351+
server.post('/', (_request, response) => {
352+
if (first) {
353+
first = false;
354+
355+
response.statusCode = 401;
356+
}
357+
358+
response.end();
359+
});
360+
361+
await t.notThrowsAsync(got.post('', {
362+
hooks: {
363+
afterResponse: [
364+
async (response, retryWithMergedOptions) => {
365+
if (response.statusCode === 401) {
366+
return retryWithMergedOptions();
367+
}
368+
369+
t.is(response.request.options.body, undefined);
370+
371+
return response;
372+
}
373+
]
374+
},
375+
form: {
376+
some: 'data'
377+
}
378+
}));
379+
});

‎test/redirects.ts

+71-9
Original file line numberDiff line numberDiff line change
@@ -120,18 +120,30 @@ test('hostname + path are not breaking redirects', withServer, async (t, server,
120120
})).body, 'reached');
121121
});
122122

123-
test('redirects only GET and HEAD requests', withServer, async (t, server, got) => {
124-
server.post('/', relativeHandler);
123+
test('redirects GET and HEAD requests', withServer, async (t, server, got) => {
124+
server.get('/', (_request, response) => {
125+
response.writeHead(308, {
126+
location: '/'
127+
});
128+
response.end();
129+
});
125130

126-
const error = await t.throwsAsync(got.post({body: 'wow'}), {
127-
instanceOf: got.HTTPError,
128-
message: 'Response code 302 (Found)'
131+
await t.throwsAsync(got.get(''), {
132+
instanceOf: got.MaxRedirectsError
129133
});
134+
});
130135

131-
// @ts-ignore
132-
t.is(error.options.path, '/');
133-
// @ts-ignore
134-
t.is(error.response.statusCode, 302);
136+
test('redirects POST requests', withServer, async (t, server, got) => {
137+
server.post('/', (_request, response) => {
138+
response.writeHead(308, {
139+
location: '/'
140+
});
141+
response.end();
142+
});
143+
144+
await t.throwsAsync(got.post({body: 'wow'}), {
145+
instanceOf: got.MaxRedirectsError
146+
});
135147
});
136148

137149
test('redirects on 303 response even on post, put, delete', withServer, async (t, server, got) => {
@@ -279,3 +291,53 @@ test('port is reset on redirect', withServer, async (t, server, got) => {
279291
const {body} = await got('');
280292
t.is(body, 'ok');
281293
});
294+
295+
test('body is reset on GET redirect', withServer, async (t, server, got) => {
296+
server.post('/', (_request, response) => {
297+
response.writeHead(303, {
298+
location: '/'
299+
});
300+
response.end();
301+
});
302+
303+
server.get('/', (_request, response) => {
304+
response.end();
305+
});
306+
307+
await got.post('', {
308+
body: 'foobar',
309+
hooks: {
310+
beforeRedirect: [
311+
options => {
312+
t.is(options.body, undefined);
313+
}
314+
]
315+
}
316+
});
317+
});
318+
319+
test('body is passed on POST redirect', withServer, async (t, server, got) => {
320+
server.post('/redirect', (_request, response) => {
321+
response.writeHead(302, {
322+
location: '/'
323+
});
324+
response.end();
325+
});
326+
327+
server.post('/', (request, response) => {
328+
request.pipe(response);
329+
});
330+
331+
const {body} = await got.post('redirect', {
332+
body: 'foobar',
333+
hooks: {
334+
beforeRedirect: [
335+
options => {
336+
t.is(options.body, 'foobar');
337+
}
338+
]
339+
}
340+
});
341+
342+
t.is(body, 'foobar');
343+
});

0 commit comments

Comments
 (0)
Please sign in to comment.