Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ky-like body response transformations #704

Merged
merged 5 commits into from
Jan 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 4 additions & 3 deletions advanced-creation.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const settings = {
return next(options);
},
options: got.mergeOptions(got.defaults.options, {
json: true
responseType: 'json'
})
};

Expand Down Expand Up @@ -110,9 +110,10 @@ const defaults = {
followRedirect: true,
stream: false,
form: false,
json: false,
cache: false,
useElectronNet: false
useElectronNet: false,
responseType: 'text',
resolveBodyOnly: 'false'
},
mutableDefaults: false
};
Expand Down
12 changes: 4 additions & 8 deletions migration-guides.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ These Got options are the same as with Request:

- [`url`](https://github.com/sindresorhus/got#url) (+ we accept [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) instances too!)
- [`body`](https://github.com/sindresorhus/got#body)
- [`json`](https://github.com/sindresorhus/got#json)
- [`followRedirect`](https://github.com/sindresorhus/got#followRedirect)
- [`encoding`](https://github.com/sindresorhus/got#encoding)

Expand Down Expand Up @@ -77,6 +76,7 @@ To use streams, just call `got.stream(url, options)` or `got(url, {stream: true,

#### Breaking changes

- The `json` option is not a `boolean`, it's an `Object`. It will be stringified and used as a body.
- No `form` option. You have to pass a [`form-data` instance](https://github.com/form-data/form-data) through the [`body` option](https://github.com/sindresorhus/got#body).
- No `oauth`/`hawk`/`aws`/`httpSignature` option. To sign requests, you need to create a [custom instance](advanced-creation.md#signing-requests).
- No `agentClass`/`agentOptions`/`pool` option.
Expand Down Expand Up @@ -105,21 +105,17 @@ const gotInstance = got.extend({
hooks: {
init: [
options => {
// Save the original option, so we can look at it in the `afterResponse` hook
options.originalJson = options.json;

if (options.json && options.jsonReplacer) {
if (options.jsonReplacer) {
options.body = JSON.stringify(options.body, options.jsonReplacer);
options.json = false; // We've handled that on our own
}
}
],
afterResponse: [
response => {
const options = response.request.gotOptions;
if (options.originalJson && options.jsonReviver) {
if (options.jsonReviver && options.responseType === 'json') {
options.responseType = '';
response.body = JSON.parse(response.body, options.jsonReviver);
options.json = false; // We've handled that on our own
}

return response;
Expand Down
113 changes: 83 additions & 30 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Got is for Node.js. For browsers, we recommend [Ky](https://github.com/sindresor
- [Handles gzip/deflate](#decompress)
- [Timeout handling](#timeout)
- [Errors with metadata](#errors)
- [JSON mode](#json)
- [JSON mode](#json-mode)
- [WHATWG URL support](#url)
- [Hooks](#hooks)
- [Instances with custom defaults](#instances)
Expand Down Expand Up @@ -157,14 +157,44 @@ Returns a `Stream` instead of a `Promise`. This is equivalent to calling `got.st

Type: `string` `Buffer` `stream.Readable` [`form-data` instance](https://github.com/form-data/form-data)

**Note:** If you provide this option, `got.stream()` will be read-only.
**Note:** The `body` option cannot be used with the `json` or `form` option.

The body that will be sent with a `POST` request.
**Note:** If you provide this option, `got.stream()` will be read-only.

If present in `options` and `options.method` is not set, `options.method` will be set to `POST`.

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`.

###### json

Type: `Object` `Array` `number` `string` `boolean` `null`

**Note:** If you provide this option, `got.stream()` will be read-only.

JSON body. The `Content-Type` header will be set to `application/json` if it's not defined.

###### responseType

Type: `string`<br>
Default: `text`

**Note:** When using streams, this option is ignored.

Parsing method used to retrieve the body from the response. Can be `text`, `json` or `buffer`. The promise has `.json()` and `.buffer()` and `.text()` functions which set this option automatically.

Example:

```js
const {body} = await got(url).json();
```

###### resolveBodyOnly

Type: `string`<br>
Default: `false`

When set to `true` the promise will return the [Response body](#body-1) instead of the [Response](#response) object.

###### cookieJar

Type: [`tough.CookieJar` instance](https://github.com/salesforce/tough-cookie#cookiejar)
Expand All @@ -182,25 +212,13 @@ Default: `'utf8'`

###### form

Type: `boolean`<br>
Default: `false`
Type: `Object`

**Note:** If you provide this option, `got.stream()` will be read-only.
**Note:** `body` must be a plain object. It will be converted to a query string using [`(new URLSearchParams(object)).toString()`](https://nodejs.org/api/url.html#url_constructor_new_urlsearchparams_obj).

If set to `true` and `Content-Type` header is not set, it will be set to `application/x-www-form-urlencoded`.
The form body is converted to query string using [`(new URLSearchParams(object)).toString()`](https://nodejs.org/api/url.html#url_constructor_new_urlsearchparams_obj).

###### json

Type: `boolean`<br>
Default: `false`

**Note:** If you use `got.stream()`, this option will be ignored.
**Note:** `body` must be a plain object or array and will be stringified.

If set to `true` and `Content-Type` header is not set, it will be set to `application/json`.

Parse response body with `JSON.parse` and set `accept` header to `application/json`. If used in conjunction with the `form` option, the `body` will the stringified as querystring and the response parsed as JSON.
If set to `true` and `Content-Type` header is not set, it will be set to `application/x-www-form-urlencoded`.

###### searchParams

Expand Down Expand Up @@ -364,19 +382,17 @@ Called with plain [request options](#options), right before their normalization.

See the [Request migration guide](migration-guides.md#breaking-changes) for an example.

**Note**: This hook must be synchronous!
**Note:** This hook must be synchronous!

###### hooks.beforeRequest

Type: `Function[]`<br>
Default: `[]`

Called with [normalized](source/normalize-arguments.js) [request options](#options). Got will make no further changes to the request before it is sent. This is especially useful in conjunction with [`got.extend()`](#instances) and [`got.create()`](advanced-creation.md) when you want to create an API client that, for example, uses HMAC-signing.
Called with [normalized](source/normalize-arguments.js) [request options](#options). Got will make no further changes to the request before it is sent (except the body serialization). This is especially useful in conjunction with [`got.extend()`](#instances) and [`got.create()`](advanced-creation.md) when you want to create an API client that, for example, uses HMAC-signing.

See the [AWS section](#aws) for an example.

**Note:** If you modify the `body` you will need to modify the `content-length` header too, because it has already been computed and assigned.

###### hooks.beforeRedirect

Type: `Function[]`<br>
Expand Down Expand Up @@ -469,7 +485,7 @@ Default: `[]`

Called with an `Error` instance. The error is passed to the hook right before it's thrown. This is especially useful when you want to have more detailed errors.

**Note**: Errors thrown while normalizing input options are thrown directly and not part of this hook.
**Note:** Errors thrown while normalizing input options are thrown directly and not part of this hook.

```js
const got = require('got');
Expand Down Expand Up @@ -505,7 +521,7 @@ Type: `Object`

##### body

Type: `string` `Object` *(depending on `options.json`)*
Type: `string` `Object` `Buffer` *(depending on `options.responseType`)*

The result of the request.

Expand Down Expand Up @@ -666,11 +682,11 @@ client.get('/demo');
'x-foo': 'bar'
}
});
const {headers} = (await client.get('/headers', {json: true})).body;
const {headers} = (await client.get('/headers').json()).body;
//=> headers['x-foo'] === 'bar'

const jsonClient = client.extend({
json: true,
responseType: 'json',
headers: {
'x-baz': 'qux'
}
Expand Down Expand Up @@ -731,7 +747,7 @@ When reading from response stream fails.

#### got.ParseError

When `json` option is enabled, server response code is 2xx, and `JSON.parse` fails. Includes `statusCode` and `statusMessage` properties.
When server response code is 2xx, and parsing body fails. Includes `body`, `statusCode` and `statusMessage` properties.

#### got.HTTPError

Expand Down Expand Up @@ -917,7 +933,7 @@ const url = 'https://api.twitter.com/1.1/statuses/home_timeline.json';

got(url, {
headers: oauth.toHeader(oauth.authorize({url, method: 'GET'}, token)),
json: true
responseType: 'json'
});
```

Expand Down Expand Up @@ -1008,6 +1024,43 @@ const createTestServer = require('create-test-server');

## Tips

### JSON mode

By default, if you pass an object to the `body` option it will be stringified using `JSON.stringify`. Example:

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

(async () => {
const response = await got('httpbin.org/anything', {
body: {
hello: 'world'
},
responseType: 'json'
});

console.log(response.body.data);
//=> '{"hello":"world"}'
})();
```

To receive a JSON body you can either set `responseType` option to `json` or use `promise.json()`. Example:

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

(async () => {
const {body} = await got('httpbin.org/anything', {
body: {
hello: 'world'
}
}).json();

console.log(body);
//=> {...}
})();
```

### User Agent

It's a good idea to set the `'user-agent'` header so the provider can more easily see how their resource is used. By default, it's the URL to this repo. You can omit this header by setting it to `null`.
Expand Down Expand Up @@ -1045,7 +1098,7 @@ const pkg = require('./package.json');

const custom = got.extend({
baseUrl: 'example.com',
json: true,
responseType: 'json',
headers: {
'user-agent': `my-package/${pkg.version} (https://github.com/username/my-package)`
}
Expand Down Expand Up @@ -1094,7 +1147,7 @@ const h2got = got.extend({request});
| Advanced timeouts | ✔ | ✖ | ✖ | ✖ | ✖ |
| Timings | ✔ | ✔ | ✖ | ✖ | ✖ |
| Errors with metadata | ✔ | ✖ | ✖ | ✔ | ✖ |
| JSON mode | ✔ | ✔ | | ✔ | ✔ |
| JSON mode | ✔ | ✔ | | ✔ | ✔ |
| Custom defaults | ✔ | ✔ | ✖ | ✔ | ✖ |
| Composable | ✔ | ✖ | ✖ | ✖ | ✔ |
| Hooks | ✔ | ✖ | ✖ | ✔ | ✖ |
Expand Down
36 changes: 32 additions & 4 deletions source/as-promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ const {reNormalize} = require('./normalize-arguments');
const asPromise = options => {
const proxy = new EventEmitter();

const parseBody = response => {
if (options.responseType === 'json') {
response.body = JSON.parse(response.body);
} else if (options.responseType === 'buffer') {
response.body = Buffer.from(response.body);
} else if (options.responseType !== 'text' && !is.falsy(options.responseType)) {
throw new Error(`Failed to parse body of type '${options.responseType}'`);
}
};

const promise = new PCancelable((resolve, reject, onCancel) => {
const emitter = requestAsEventEmitter(options);

Expand Down Expand Up @@ -57,9 +67,9 @@ const asPromise = options => {

const {statusCode} = response;

if (options.json && response.body) {
if (response.body) {
try {
response.body = JSON.parse(response.body);
parseBody(response);
} catch (error) {
if (statusCode >= 200 && statusCode < 300) {
const parseError = new ParseError(error, statusCode, options, data);
Expand All @@ -79,13 +89,13 @@ const asPromise = options => {
return;
}

resolve(response);
resolve(options.resolveBodyOnly ? response.body : response);
}

return;
}

resolve(response);
resolve(options.resolveBodyOnly ? response.body : response);
});

emitter.once('error', reject);
Expand All @@ -102,6 +112,24 @@ const asPromise = options => {
return promise;
};

promise.json = () => {
options.responseType = 'json';
options.resolveBodyOnly = true;
return promise;
};

promise.buffer = () => {
options.responseType = 'buffer';
options.resolveBodyOnly = true;
return promise;
};

promise.text = () => {
options.responseType = 'text';
options.resolveBodyOnly = true;
return promise;
};

return promise;
};

Expand Down
3 changes: 2 additions & 1 deletion source/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ module.exports.ReadError = class extends GotError {

module.exports.ParseError = class extends GotError {
constructor(error, statusCode, options, data) {
super(`${error.message} in "${urlLib.format(options)}": \n${data.slice(0, 77)}...`, error, options);
super(`${error.message} in "${urlLib.format(options)}"`, error, options);
this.name = 'ParseError';
this.body = data;
this.statusCode = statusCode;
this.statusMessage = http.STATUS_CODES[this.statusCode];
}
Expand Down
6 changes: 3 additions & 3 deletions source/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ const defaults = {
throwHttpErrors: true,
followRedirect: true,
stream: false,
form: false,
json: false,
cache: false,
useElectronNet: false
useElectronNet: false,
responseType: 'text',
resolveBodyOnly: false
},
mutableDefaults: false
};
Expand Down