diff --git a/readme.md b/readme.md index 90f3f053a..6f527ed23 100644 --- a/readme.md +++ b/readme.md @@ -784,6 +784,12 @@ Type: `string | object | Buffer` *(Depending on `options.responseType`)* The result of the request. +##### rawBody + +Type: `Buffer` + +The raw result of the request. + ##### url Type: `string` diff --git a/source/as-promise/core.ts b/source/as-promise/core.ts index d4ceaf3a6..767975d22 100644 --- a/source/as-promise/core.ts +++ b/source/as-promise/core.ts @@ -17,18 +17,20 @@ if (!knownHookEvents.includes('beforeRetry' as any)) { export const knownBodyTypes = ['json', 'buffer', 'text']; // @ts-ignore The error is: Not all code paths return a value. -export const parseBody = (body: Buffer, response: Response, responseType: ResponseType, encoding?: string): unknown => { +export const parseBody = (response: Response, responseType: ResponseType, encoding?: string): unknown => { + const {rawBody} = response; + try { if (responseType === 'text') { - return body.toString(encoding); + return rawBody.toString(encoding); } if (responseType === 'json') { - return body.length === 0 ? '' : JSON.parse(body.toString()) as unknown; + return rawBody.length === 0 ? '' : JSON.parse(rawBody.toString()) as unknown; } if (responseType === 'buffer') { - return Buffer.from(body); + return Buffer.from(rawBody); } if (!knownBodyTypes.includes(responseType)) { @@ -42,7 +44,6 @@ export const parseBody = (body: Buffer, response: Response, responseType: Respon export default class PromisableRequest extends Request { ['constructor']: typeof PromisableRequest; declare options: NormalizedOptions; - declare _throwHttpErrors: boolean; static normalizeArguments(url?: string | URL, nonNormalizedOptions?: Options, defaults?: Defaults): NormalizedOptions { const options = super.normalizeArguments(url, nonNormalizedOptions, defaults) as NormalizedOptions; @@ -119,6 +120,11 @@ export default class PromisableRequest extends Request { } } + // JSON mode + if (options.responseType === 'json' && options.headers.accept === undefined) { + options.headers.accept = 'application/json'; + } + return options; } diff --git a/source/as-promise/index.ts b/source/as-promise/index.ts index 1e039a085..eca121cf5 100644 --- a/source/as-promise/index.ts +++ b/source/as-promise/index.ts @@ -23,34 +23,28 @@ const proxiedRequestEvents = [ export default function asPromise(options: NormalizedOptions): CancelableRequest { let retryCount = 0; - let body: Buffer; - let currentResponse: Response; + let globalRequest: PromisableRequest; + let globalResponse: Response; const emitter = new EventEmitter(); const promise = new PCancelable((resolve, reject, onCancel) => { const makeRequest = (): void => { - if (options.responseType === 'json' && options.headers.accept === undefined) { - options.headers.accept = 'application/json'; - } - // Support retries // `options.throwHttpErrors` needs to be always true, // so the HTTP errors are caught and the request is retried. - // The error is **eventaully** thrown - // if the user value `options._throwHttpErrors` is true. + // The error is **eventually** thrown if the user value is true. const {throwHttpErrors} = options; if (!throwHttpErrors) { options.throwHttpErrors = true; } const request = new PromisableRequest(options.url, options); - request._throwHttpErrors = throwHttpErrors; request._noPipe = true; onCancel(() => request.destroy()); - request.once('response', async (response: Response) => { - currentResponse = response; + globalRequest = request; + request.once('response', async (response: Response) => { response.retryCount = retryCount; if (response.request.aborted) { @@ -66,8 +60,11 @@ export default function asPromise(options: NormalizedOptions): CancelableRequ }; // Download body + let rawBody; try { - body = await getStream.buffer(request); + rawBody = await getStream.buffer(request); + + response.rawBody = rawBody; } catch (error) { request._beforeError(new ReadError(error, options, response)); return; @@ -75,10 +72,10 @@ export default function asPromise(options: NormalizedOptions): CancelableRequ // Parse body try { - response.body = parseBody(body, response, options.responseType, options.encoding); + response.body = parseBody(response, options.responseType, options.encoding); } catch (error) { // Fallback to `utf8` - response.body = body.toString('utf8'); + response.body = rawBody.toString(); if (isOk()) { request._beforeError(error); @@ -131,6 +128,8 @@ export default function asPromise(options: NormalizedOptions): CancelableRequ return; } + globalResponse = response; + resolve(options.resolveBodyOnly ? response.body as T : response as unknown as T); }); @@ -225,7 +224,7 @@ export default function asPromise(options: NormalizedOptions): CancelableRequ // Wait until downloading has ended await promise; - return parseBody(body, currentResponse, responseType); + return parseBody(globalResponse, responseType, options.encoding); })(); Object.defineProperties(newPromise, Object.getOwnPropertyDescriptors(promise)); @@ -234,7 +233,7 @@ export default function asPromise(options: NormalizedOptions): CancelableRequ }; promise.json = () => { - if (body === undefined && options.headers.accept === undefined) { + if (!globalRequest.writableFinished && options.headers.accept === undefined) { options.headers.accept = 'application/json'; } diff --git a/source/core/index.ts b/source/core/index.ts index 1dc1119e2..a9876dc05 100644 --- a/source/core/index.ts +++ b/source/core/index.ts @@ -221,6 +221,7 @@ export interface PlainResponse extends IncomingMessageWithTimings { // For Promise support export interface Response extends PlainResponse { body: T; + rawBody: Buffer; retryCount: number; } @@ -1313,11 +1314,12 @@ export default class Request extends Duplex implements RequestEvents { try { const {response} = error as RequestError; - if (response && is.undefined(response.body)) { - response.body = await getStream(response, { - ...this.options, - encoding: (this as any)._readableState.encoding - }); + + if (response) { + response.setEncoding((this as any)._readableState.encoding); + + response.rawBody = await getStream.buffer(response); + response.body = response.rawBody.toString(); } } catch (_) {} diff --git a/test/response-parse.ts b/test/response-parse.ts index 132a6c046..724894b1c 100644 --- a/test/response-parse.ts +++ b/test/response-parse.ts @@ -182,3 +182,43 @@ test('shortcuts throw ParseErrors', withServer, async (t, server, got) => { message: /^Unexpected token o in JSON at position 1 in/ }); }); + +test('shortcuts result properly when retrying in afterResponse', withServer, async (t, server, got) => { + const nasty = JSON.stringify({hello: 'nasty'}); + const proper = JSON.stringify({hello: 'world'}); + + server.get('/', (request, response) => { + if (request.headers.token === 'unicorn') { + response.end(proper); + } else { + response.statusCode = 401; + response.end(nasty); + } + }); + + const promise = got({ + hooks: { + afterResponse: [ + (response, retryWithMergedOptions) => { + if (response.statusCode === 401) { + return retryWithMergedOptions({ + headers: { + token: 'unicorn' + } + }); + } + + return response; + } + ] + } + }); + + const json = await promise.json<{hello: string}>(); + const text = await promise.text(); + const buffer = await promise.buffer(); + + t.is(json.hello, 'world'); + t.is(text, proper); + t.is(buffer.compare(Buffer.from(proper)), 0); +});