Skip to content

Commit

Permalink
Fix the responseType option
Browse files Browse the repository at this point in the history
If `.buffer()` is used, it will use `binary` encoding.
Otherwise it will use `options.encoding`.

Fixes #958
  • Loading branch information
szmarczak committed Dec 8, 2019
1 parent 62cdeec commit 071bf5e
Show file tree
Hide file tree
Showing 8 changed files with 67 additions and 59 deletions.
11 changes: 3 additions & 8 deletions readme.md
Expand Up @@ -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:

Expand Down
79 changes: 39 additions & 40 deletions source/as-promise.ts
Expand Up @@ -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<T>(options: NormalizedOptions): CancelableRequest<T> {
const proxy = new EventEmitter();
let finalResponse: Pick<Response, 'body' | 'statusCode'>;
let body: Buffer;

// @ts-ignore `.json()`, `.buffer()` and `.text()` are added later
const promise = new PCancelable<Response | Response['body']>((resolve, reject, onCancel) => {
Expand All @@ -52,8 +48,9 @@ export default function asPromise<T>(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;
Expand All @@ -64,6 +61,27 @@ export default function asPromise<T>(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
Expand All @@ -75,7 +93,6 @@ export default function asPromise<T>(options: NormalizedOptions): CancelableRequ
calculateDelay: () => 0
},
throwHttpErrors: false,
responseType: 'text',
resolveBodyOnly: false
}));

Expand Down Expand Up @@ -103,36 +120,18 @@ export default function asPromise<T>(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);
Expand All @@ -150,15 +149,15 @@ export default function asPromise<T>(options: NormalizedOptions): CancelableRequ

const shortcut = <T>(responseType: NormalizedOptions['responseType']): CancelableRequest<T> => {
// 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));

return newPromise as CancelableRequest<T>;
};

promise.json = () => {
if (is.undefined(options.headers.accept)) {
if (is.undefined(body) && is.undefined(options.headers.accept)) {
options.headers.accept = 'application/json';
}

Expand Down
10 changes: 8 additions & 2 deletions source/as-stream.ts
Expand Up @@ -64,13 +64,19 @@ export default function asStream<T>(options: NormalizedOptions): ProxyStream<T>
}

{
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,
Expand Down
5 changes: 1 addition & 4 deletions source/get-response.ts
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion source/index.ts
Expand Up @@ -55,7 +55,7 @@ const defaults: Defaults = {
cache: false,
dnsCache: false,
useElectronNet: false,
responseType: 'default',
responseType: 'text',
resolveBodyOnly: false,
maxRedirects: 10,
prefixUrl: '',
Expand Down
2 changes: 1 addition & 1 deletion source/utils/types.ts
Expand Up @@ -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<BodyType = unknown> extends http.IncomingMessage {
body: BodyType;
Expand Down
4 changes: 2 additions & 2 deletions test/hooks.ts
Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down
13 changes: 12 additions & 1 deletion test/response-parse.ts
Expand Up @@ -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<ParseError>(got({responseType: 'invalid'}), /^Failed to parse body of type 'string' as 'invalid'/);
const error = await t.throwsAsync<ParseError>(got({responseType: 'invalid'}), /^Unknown body type 'invalid'/);
t.true(error.message.includes(error.options.url.hostname));
t.is(error.options.url.pathname, '/');
});
Expand Down Expand Up @@ -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);
});

0 comments on commit 071bf5e

Please sign in to comment.