diff --git a/readme.md b/readme.md index 2fda8f6e9..f37158f4c 100644 --- a/readme.md +++ b/readme.md @@ -238,18 +238,13 @@ const instance = got.extend({ ###### responseType Type: `string`\ -Default: `'default'` +Default: `'text'` **Note:** When using streams, this option is ignored. -Parsing method used to retrieve the body from the response. +The parsing method. Can be `'text'`, `'json'` or `'buffer'`. -- `'default'` - Will give a string unless the body is overwritten in a `afterResponse` hook or if `options.decompress` is set to false - Will give a Buffer if the response is compresssed. -- `'text'` - Will give a string no matter what. -- `'json'` - Will give an object, unless the body is invalid JSON, then it will throw. -- `'buffer'` - Will give a Buffer, ignoring `options.encoding`. It will throw if the body is a custom object. - -The promise has `.json()` and `.buffer()` and `.text()` methods which set this option automatically. +The promise has also `.text()`, `.json()` and `.buffer()` methods which set this option automatically. Example: diff --git a/source/as-promise.ts b/source/as-promise.ts index bb521aa4a..14345e232 100644 --- a/source/as-promise.ts +++ b/source/as-promise.ts @@ -7,29 +7,25 @@ import {normalizeArguments, mergeOptions} from './normalize-arguments'; import requestAsEventEmitter, {proxyEvents} from './request-as-event-emitter'; import {CancelableRequest, GeneralError, NormalizedOptions, Response} from './utils/types'; -const parseBody = (body: Response['body'], responseType: NormalizedOptions['responseType'], statusCode: Response['statusCode']): unknown => { - if (responseType === 'json' && is.string(body)) { - return statusCode === 204 ? '' : JSON.parse(body); +const parseBody = (body: Buffer, responseType: NormalizedOptions['responseType'], encoding: NormalizedOptions['encoding']): unknown => { + if (responseType === 'json') { + return body.length === 0 ? '' : JSON.parse(body.toString()); } - if (responseType === 'buffer' && is.string(body)) { + if (responseType === 'buffer') { return Buffer.from(body); } if (responseType === 'text') { - return String(body); + return body.toString(encoding); } - if (responseType === 'default') { - return body; - } - - throw new Error(`Failed to parse body of type '${typeof body}' as '${responseType!}'`); + throw new TypeError(`Unknown body type '${responseType!}'`); }; export default function asPromise(options: NormalizedOptions): CancelableRequest { const proxy = new EventEmitter(); - let finalResponse: Pick; + let body: Buffer; // @ts-ignore `.json()`, `.buffer()` and `.text()` are added later const promise = new PCancelable((resolve, reject, onCancel) => { @@ -52,8 +48,9 @@ export default function asPromise(options: NormalizedOptions): CancelableRequ emitter.on('response', async (response: Response) => { proxy.emit('response', response); + // Download body try { - response.body = await getStream(response, {encoding: options.encoding}); + body = await getStream.buffer(response, {encoding: 'binary'}); } catch (error) { emitError(new ReadError(error, options)); return; @@ -64,6 +61,27 @@ export default function asPromise(options: NormalizedOptions): CancelableRequ return; } + const isOk = () => { + const {statusCode} = response; + const limitStatusCode = options.followRedirect ? 299 : 399; + + return (statusCode >= 200 && statusCode <= limitStatusCode) || statusCode === 304; + }; + + // Parse body + try { + response.body = parseBody(body, options.responseType, options.encoding); + } catch (error) { + if (isOk()) { + const parseError = new ParseError(error, response, options); + emitError(parseError); + return; + } + + // Fallback to `utf8` + response.body = body.toString(); + } + try { for (const [index, hook] of options.hooks.afterResponse.entries()) { // @ts-ignore Promise is not assignable to CancelableRequest @@ -75,7 +93,6 @@ export default function asPromise(options: NormalizedOptions): CancelableRequ calculateDelay: () => 0 }, throwHttpErrors: false, - responseType: 'text', resolveBodyOnly: false })); @@ -103,36 +120,18 @@ export default function asPromise(options: NormalizedOptions): CancelableRequ return; } - const {statusCode} = response; - - finalResponse = { - body: response.body, - statusCode - }; + // Check for HTTP error codes + if (!isOk()) { + const error = new HTTPError(response, options); - try { - response.body = parseBody(response.body, options.responseType, response.statusCode); - } catch (error) { - if (statusCode >= 200 && statusCode < 300) { - const parseError = new ParseError(error, response, options); - emitError(parseError); + if (emitter.retry(error)) { return; } - } - - const limitStatusCode = options.followRedirect ? 299 : 399; - if (statusCode !== 304 && (statusCode < 200 || statusCode > limitStatusCode)) { - const error = new HTTPError(response, options); - if (!emitter.retry(error)) { - if (options.throwHttpErrors) { - emitError(error); - return; - } - resolve(options.resolveBodyOnly ? response.body : response); + if (options.throwHttpErrors) { + emitError(error); + return; } - - return; } resolve(options.resolveBodyOnly ? response.body : response); @@ -150,7 +149,7 @@ export default function asPromise(options: NormalizedOptions): CancelableRequ const shortcut = (responseType: NormalizedOptions['responseType']): CancelableRequest => { // eslint-disable-next-line promise/prefer-await-to-then - const newPromise = promise.then(() => parseBody(finalResponse.body, responseType, finalResponse.statusCode)); + const newPromise = promise.then(() => parseBody(body, responseType, options.encoding)); Object.defineProperties(newPromise, Object.getOwnPropertyDescriptors(promise)); @@ -158,7 +157,7 @@ export default function asPromise(options: NormalizedOptions): CancelableRequ }; promise.json = () => { - if (is.undefined(options.headers.accept)) { + if (is.undefined(body) && is.undefined(options.headers.accept)) { options.headers.accept = 'application/json'; } diff --git a/source/as-stream.ts b/source/as-stream.ts index c132cc27c..37da592ff 100644 --- a/source/as-stream.ts +++ b/source/as-stream.ts @@ -64,13 +64,19 @@ export default function asStream(options: NormalizedOptions): ProxyStream } { - const read = proxy._read.bind(proxy); + const read = proxy._read; proxy._read = (...args) => { isFinished = true; - return read(...args); + + proxy._read = read; + return read.apply(proxy, args); }; } + if (options.encoding) { + proxy.setEncoding(options.encoding); + } + stream.pipeline( response, output, diff --git a/source/get-response.ts b/source/get-response.ts index ff52b0cf3..781dfbc0b 100644 --- a/source/get-response.ts +++ b/source/get-response.ts @@ -21,10 +21,7 @@ export default async (response: IncomingMessage, options: NormalizedOptions, emi ); if (!options.decompress && ['gzip', 'deflate', 'br'].includes(response.headers['content-encoding'] ?? '')) { - options.responseType = 'default'; - - // @ts-ignore Internal use. - options.encoding = 'buffer'; + options.responseType = 'buffer'; } emitter.emit('response', newResponse); diff --git a/source/index.ts b/source/index.ts index 13090e324..50da82ae8 100644 --- a/source/index.ts +++ b/source/index.ts @@ -55,7 +55,7 @@ const defaults: Defaults = { cache: false, dnsCache: false, useElectronNet: false, - responseType: 'default', + responseType: 'text', resolveBodyOnly: false, maxRedirects: 10, prefixUrl: '', diff --git a/source/utils/types.ts b/source/utils/types.ts index e9acbeac9..de94fd7f2 100644 --- a/source/utils/types.ts +++ b/source/utils/types.ts @@ -43,7 +43,7 @@ export type ErrorCode = | 'ENETUNREACH' | 'EAI_AGAIN'; -export type ResponseType = 'json' | 'buffer' | 'text' | 'default'; +export type ResponseType = 'json' | 'buffer' | 'text'; export interface Response extends http.IncomingMessage { body: BodyType; diff --git a/test/hooks.ts b/test/hooks.ts index d1ef53b28..19645c459 100644 --- a/test/hooks.ts +++ b/test/hooks.ts @@ -330,7 +330,7 @@ test('afterResponse is called with response', withServer, async (t, server, got) hooks: { afterResponse: [ response => { - t.is(typeof response.body, 'string'); + t.is(typeof response.body, 'object'); return response; } @@ -347,7 +347,7 @@ test('afterResponse allows modifications', withServer, async (t, server, got) => hooks: { afterResponse: [ response => { - response.body = '{"hello": "world"}'; + response.body = {hello: 'world'}; return response; } diff --git a/test/response-parse.ts b/test/response-parse.ts index 69b2f9ac6..6dba1489b 100644 --- a/test/response-parse.ts +++ b/test/response-parse.ts @@ -79,7 +79,7 @@ test('throws an error on invalid response type', withServer, async (t, server, g server.get('/', defaultHandler); // @ts-ignore Error tests - const error = await t.throwsAsync(got({responseType: 'invalid'}), /^Failed to parse body of type 'string' as 'invalid'/); + const error = await t.throwsAsync(got({responseType: 'invalid'}), /^Unknown body type 'invalid'/); t.true(error.message.includes(error.options.url.hostname)); t.is(error.options.url.pathname, '/'); }); @@ -148,3 +148,14 @@ test('doesn\'t throw on 204 No Content', withServer, async (t, server, got) => { const body = await got('').json(); t.is(body, ''); }); + +test('.buffer() returns binary content', withServer, async (t, server, got) => { + const body = Buffer.from('89504E470D0A1A0A0000000D49484452', 'hex'); + + server.get('/', (_request, response) => { + response.end(body); + }); + + const buffer = await got('').buffer(); + t.is(Buffer.compare(buffer, body), 0); +});