Skip to content

Commit

Permalink
Add options to customize parsing/stringifying JSON (sindresorhus#1298)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
Co-authored-by: Szymon Marczak <36894700+szmarczak@users.noreply.github.com>
  • Loading branch information
3 people committed Jul 6, 2020
1 parent eff11b0 commit d73216b
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 42 deletions.
32 changes: 3 additions & 29 deletions documentation/migration-guides.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,38 +97,12 @@ const gotInstance = got.extend({
gotInstance(url, options);
```

- No `jsonReviver`/`jsonReplacer` option, but you can use hooks for that too:
- No `jsonReviver`/`jsonReplacer` option, but you can use `parseJson`/`stringifyJson` for that:

```js
const gotInstance = got.extend({
hooks: {
init: [
options => {
if (options.jsonReplacer && options.json) {
options.body = JSON.stringify(options.json, options.jsonReplacer);
delete options.json;
}
}
],
beforeRequest: [
options => {
if (options.responseType === 'json' && options.jsonReviver) {
options.responseType = 'text';
options.customJsonResponse = true;
}
}
],
afterResponse: [
response => {
const {options} = response.request;
if (options.jsonReviver && options.customJsonResponse) {
response.body = JSON.parse(response.body, options.jsonReviver);
}

return response;
}
]
}
parseJson: text => JSON.parse(text, myJsonReviver),
stringifyJson: object => JSON.stringify(object, myJsonReplacer)
});

gotInstance(url, options);
Expand Down
83 changes: 82 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,87 @@ const body = await got(url).json();
const body = await got(url, {responseType: 'json', resolveBodyOnly: true});
```

###### parseJson

Type: `(text: string) => unknown`\
Default: `(text: string) => JSON.parse(text)`

A function used to parse JSON responses.

<details>
<summary>Example</summary>

Using [`bourne`](https://github.com/hapijs/bourne) to prevent prototype pollution:

```js
const got = require('got');
const Bourne = require('@hapi/bourne');

(async () => {
const parsed = await got('https://example.com', {
parseJson: text => Bourne.parse(text)
}).json();

console.log(parsed);
})();
```
</details>

###### stringifyJson

Type: `(object: unknown) => string`\
Default: `(object: unknown) => JSON.stringify(object)`

A function used to stringify the body of JSON requests.

<details>
<summary>Examples</summary>

Ignore properties starting with `_`:

```js
const got = require('got');

(async () => {
await got.post('https://example.com', {
stringifyJson: object => JSON.stringify(object, (key, value) => {
if (key.startsWith('_')) {
return;
}

return value;
}),
json: {
some: 'payload',
_ignoreMe: 1234
}
});
})();
```

All numbers as strings:

```js
const got = require('got');

(async () => {
await got.post('https://example.com', {
stringifyJson: object => JSON.stringify(object, (key, value) => {
if (typeof value === 'number') {
return value.toString();
}

return value;
}),
json: {
some: 'payload',
number: 1
}
});
})();
```
</details>

###### resolveBodyOnly

Type: `boolean`\
Expand Down Expand Up @@ -1000,7 +1081,7 @@ await got('https://example.com', {
if (hostname === 'example.com') {
return; // Certificate OK
}

return new Error('Invalid Hostname'); // Certificate NOT OK
}
}
Expand Down
6 changes: 3 additions & 3 deletions source/as-promise/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
ParseError,
Response
} from './types';
import Request, {knownHookEvents, RequestError, Method} from '../core';
import Request, {knownHookEvents, RequestError, Method, ParseJsonFunction} from '../core';

if (!knownHookEvents.includes('beforeRetry' as any)) {
knownHookEvents.push('beforeRetry' as any, 'afterResponse' as any);
Expand All @@ -17,7 +17,7 @@ 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 = (response: Response, responseType: ResponseType, encoding?: string): unknown => {
export const parseBody = (response: Response, responseType: ResponseType, parseJson: ParseJsonFunction, encoding?: string): unknown => {
const {rawBody} = response;

try {
Expand All @@ -26,7 +26,7 @@ export const parseBody = (response: Response, responseType: ResponseType, encodi
}

if (responseType === 'json') {
return rawBody.length === 0 ? '' : JSON.parse(rawBody.toString()) as unknown;
return rawBody.length === 0 ? '' : parseJson(rawBody.toString());
}

if (responseType === 'buffer') {
Expand Down
4 changes: 2 additions & 2 deletions source/as-promise/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export default function asPromise<T>(options: NormalizedOptions): CancelableRequ

// Parse body
try {
response.body = parseBody(response, options.responseType, options.encoding);
response.body = parseBody(response, options.responseType, options.parseJson, options.encoding);
} catch (error) {
// Fallback to `utf8`
response.body = rawBody.toString();
Expand Down Expand Up @@ -244,7 +244,7 @@ export default function asPromise<T>(options: NormalizedOptions): CancelableRequ
// Wait until downloading has ended
await promise;

return parseBody(globalResponse, responseType, options.encoding);
return parseBody(globalResponse, responseType, options.parseJson, options.encoding);
})();

Object.defineProperties(newPromise, Object.getOwnPropertyDescriptors(promise));
Expand Down
20 changes: 14 additions & 6 deletions source/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,17 @@ export type RequestFunction = (url: URL, options: RequestOptions, callback?: (re

export type Headers = Record<string, string | string[] | undefined>;

type CacheableRequestFn = (
type CacheableRequestFunction = (
opts: string | URL | RequestOptions,
cb?: (response: ServerResponse | ResponseLike) => void
) => CacheableRequest.Emitter;

type CheckServerIdentityFn = (hostname: string, certificate: DetailedPeerCertificate) => Error | void;
type CheckServerIdentityFunction = (hostname: string, certificate: DetailedPeerCertificate) => Error | void;
export type ParseJsonFunction = (text: string) => unknown;
export type StringifyJsonFunction = (object: unknown) => string;

interface RealRequestOptions extends https.RequestOptions {
checkServerIdentity: CheckServerIdentityFn;
checkServerIdentity: CheckServerIdentityFunction;
}

export interface Options extends URLOptions {
Expand Down Expand Up @@ -152,6 +154,8 @@ export interface Options extends URLOptions {
headers?: Headers;
methodRewriting?: boolean;
dnsLookupIpVersion?: DnsLookupIpVersion;
parseJson?: ParseJsonFunction;
stringifyJson?: StringifyJsonFunction;

// From `http.RequestOptions`
localAddress?: string;
Expand All @@ -170,7 +174,7 @@ export interface HTTPSOptions {
rejectUnauthorized?: https.RequestOptions['rejectUnauthorized'];

// From `tls.ConnectionOptions`
checkServerIdentity?: CheckServerIdentityFn;
checkServerIdentity?: CheckServerIdentityFunction;

// From `tls.SecureContextOptions`
certificateAuthority?: SecureContextOptions['ca'];
Expand Down Expand Up @@ -203,6 +207,8 @@ export interface NormalizedOptions extends Options {
methodRewriting: boolean;
username: string;
password: string;
parseJson: ParseJsonFunction;
stringifyJson: StringifyJsonFunction;
[kRequest]: HttpRequestFunction;
[kIsNormalizedAlready]?: boolean;
}
Expand All @@ -226,6 +232,8 @@ export interface Defaults {
allowGetBody: boolean;
https?: HTTPSOptions;
methodRewriting: boolean;
parseJson: ParseJsonFunction;
stringifyJson: StringifyJsonFunction;

// Optional
agent?: Agents | false;
Expand Down Expand Up @@ -282,7 +290,7 @@ function isClientRequest(clientRequest: unknown): clientRequest is ClientRequest
return is.object(clientRequest) && !('statusCode' in clientRequest);
}

const cacheableStore = new WeakableMap<string | CacheableRequest.StorageAdapter, CacheableRequestFn>();
const cacheableStore = new WeakableMap<string | CacheableRequest.StorageAdapter, CacheableRequestFunction>();

const waitForOpenFile = async (file: ReadStream): Promise<void> => new Promise((resolve, reject) => {
const onError = (error: Error): void => {
Expand Down Expand Up @@ -996,7 +1004,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {
headers['content-type'] = 'application/json';
}

this[kBody] = JSON.stringify(options.json);
this[kBody] = options.stringifyJson(options.json);
}

const uploadBodySize = await getBodySize(this[kBody], options.headers);
Expand Down
4 changes: 3 additions & 1 deletion source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ const defaults: InstanceDefaults = {
backoff: 0,
requestLimit: 10000,
stackAllItems: true
}
},
parseJson: (text: string) => JSON.parse(text),
stringifyJson: (object: unknown) => JSON.stringify(object)
},
handlers: [defaultHandler],
mutableDefaults: false
Expand Down
16 changes: 16 additions & 0 deletions test/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const echoIp: Handler = (request, response) => {
response.end(address === '::ffff:127.0.0.1' ? '127.0.0.1' : address);
};

const echoBody: Handler = async (request, response) => {
response.end(await getStream(request));
};

test('simple request', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.end('ok');
Expand Down Expand Up @@ -351,3 +355,15 @@ test.serial('deprecated `family` option', withServer, async (t, server, got) =>
})();
});
});

test('JSON request custom stringifier', withServer, async (t, server, got) => {
server.post('/', echoBody);

const payload = {a: 'b'};
const customStringify = (object: any) => JSON.stringify({...object, c: 'd'});

t.deepEqual((await got.post({
stringifyJson: customStringify,
json: payload
})).body, customStringify(payload));
});
9 changes: 9 additions & 0 deletions test/response-parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,12 @@ test('responseType is optional when using template', withServer, async (t, serve

t.deepEqual(body, data);
});

test('JSON response custom parser', withServer, async (t, server, got) => {
server.get('/', defaultHandler);

t.deepEqual((await got({
responseType: 'json',
parseJson: text => ({...JSON.parse(text), custom: 'parser'})
})).body, {...dog, custom: 'parser'});
});

0 comments on commit d73216b

Please sign in to comment.