diff --git a/readme.md b/readme.md index 9205e2d6c..38782783e 100644 --- a/readme.md +++ b/readme.md @@ -188,7 +188,7 @@ Type: `string | Buffer | stream.Readable` or [`form-data` instance](https://gith **Note #2:** If you provide this option, `got.stream()` will be read-only. -**Note #3:** If you provide a payload with the `GET` or `HEAD` method, it will throw a `TypeError`. +**Note #3:** If you provide a payload with the `GET` or `HEAD` method, it will throw a `TypeError` unless the method is `GET` and the `allowGetBody` option is set to `true`. The `content-length` header will be automatically set if `body` is a `string` / `Buffer` / `fs.createReadStream` instance / [`form-data` instance](https://github.com/form-data/form-data), and `content-length` and `transfer-encoding` are not manually set in `options.headers`. @@ -410,6 +410,15 @@ Default: `true` By default, redirects will use [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 the body to the new location using the same HTTP method (`POST` in this case). +###### allowGetBody + +Type: `boolean`\ +Default: `false` + +**Note:** The [RFC 7321](https://tools.ietf.org/html/rfc7231#section-4.3.1) doesn't specify any particular behavior for the GET method having a payload, therefore **it's considered an [anti-pattern](https://en.wikipedia.org/wiki/Anti-pattern)**. + +Set this to `true` to allow sending body for the `GET` method. However, the [HTTP/2 specification](https://tools.ietf.org/html/rfc7540#section-8.1.3) says that `An HTTP GET request includes request header fields and no payload body`, therefore when using the HTTP/2 protocol this option will have no effect. This option is only meant to interact with non-compliant servers when you have no other choice. + ###### maxRedirects Type: `number`\ diff --git a/source/as-stream.ts b/source/as-stream.ts index aebfb243f..4a942abda 100644 --- a/source/as-stream.ts +++ b/source/as-stream.ts @@ -23,7 +23,7 @@ export default function asStream(options: NormalizedOptions): ProxyStream proxy.destroy(); throw new Error('Got\'s stream is not writable when the `body`, `json` or `form` option is used'); }; - } else if (options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH') { + } else if (options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH' || (options.allowGetBody && options.method === 'GET')) { options.body = input; } else { proxy.write = () => { diff --git a/source/index.ts b/source/index.ts index 71b7d512f..1dbf44679 100644 --- a/source/index.ts +++ b/source/index.ts @@ -64,6 +64,7 @@ const defaults: Defaults = { maxRedirects: 10, prefixUrl: '', methodRewriting: true, + allowGetBody: false, ignoreInvalidCookies: false, context: {}, _pagination: { diff --git a/source/normalize-arguments.ts b/source/normalize-arguments.ts index 1c7a68df0..831b7981c 100644 --- a/source/normalize-arguments.ts +++ b/source/normalize-arguments.ts @@ -231,6 +231,7 @@ export const preNormalizeArguments = (options: Options, defaults?: NormalizedOpt options.dnsCache = options.dnsCache ?? false; options.useElectronNet = Boolean(options.useElectronNet); options.methodRewriting = Boolean(options.methodRewriting); + options.allowGetBody = Boolean(options.allowGetBody); options.context = options.context ?? {}; return options as NormalizedOptions; @@ -362,7 +363,8 @@ export const normalizeArguments = (url: URLOrOptions, options?: Options, default return normalizedOptions; }; -const withoutBody: ReadonlySet = new Set(['GET', 'HEAD']); +const withoutBody: ReadonlySet = new Set(['HEAD']); +const withoutBodyUnlessSpecified = 'GET'; export type NormalizedRequestArguments = Merge isTrue).length > 1) { throw new TypeError('The `body`, `json` and `form` options are mutually exclusive'); } @@ -441,7 +447,7 @@ export const normalizeRequestArguments = async (options: NormalizedOptions): Pro // body. if (is.undefined(headers['content-length']) && is.undefined(headers['transfer-encoding'])) { if ( - (options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH' || options.method === 'DELETE') && + (options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH' || options.method === 'DELETE' || (options.allowGetBody && options.method === 'GET')) && !is.undefined(uploadBodySize) ) { // @ts-ignore We assign if it is undefined, so this IS correct diff --git a/source/types.ts b/source/types.ts index e6f03a193..4be4eccdf 100644 --- a/source/types.ts +++ b/source/types.ts @@ -199,6 +199,7 @@ export interface GotOptions extends PaginationOptions { context?: {[key: string]: any}; maxRedirects?: number; lookup?: CacheableLookup['lookup']; + allowGetBody?: boolean; methodRewriting?: boolean; } @@ -232,6 +233,7 @@ export interface NormalizedOptions extends Options { followRedirect: boolean; useElectronNet: boolean; methodRewriting: boolean; + allowGetBody: boolean; context: {[key: string]: any}; // UNIX socket support diff --git a/test/arguments.ts b/test/arguments.ts index c5cbd42eb..9ed09fd29 100644 --- a/test/arguments.ts +++ b/test/arguments.ts @@ -162,6 +162,13 @@ test('throws when passing body with a non payload method', async t => { }); }); +test('`allowGetBody` option', withServer, async (t, server, got) => { + server.get('/test', echoUrl); + + const url = new URL(`${server.url}/test`); + await t.notThrowsAsync(got(url, {body: 'asdf', allowGetBody: true})); +}); + test('WHATWG URL support', withServer, async (t, server, got) => { server.get('/test', echoUrl); diff --git a/test/post.ts b/test/post.ts index baa689c1b..db3691c05 100644 --- a/test/post.ts +++ b/test/post.ts @@ -20,12 +20,18 @@ const echoHeaders: Handler = (request, response) => { response.end(JSON.stringify(request.headers)); }; -test('GET cannot have body', withServer, async (t, server, got) => { +test('GET cannot have body without the `allowGetBody` option', withServer, async (t, server, got) => { server.post('/', defaultEndpoint); await t.throwsAsync(got.get({body: 'hi'}), {message: 'The `GET` method cannot be used with a body'}); }); +test('GET can have body with option allowGetBody', withServer, async (t, server, got) => { + server.get('/', defaultEndpoint); + + await t.notThrowsAsync(got.get({body: 'hi', allowGetBody: true})); +}); + test('invalid body', async t => { await t.throwsAsync( // @ts-ignore Error tests