From 46e85e94b231af4b2c98e7312ac722cbf15d409b Mon Sep 17 00:00:00 2001 From: Szymon Marczak <36894700+szmarczak@users.noreply.github.com> Date: Thu, 17 Jan 2019 19:06:23 +0100 Subject: [PATCH] Ky-like body response transformations (#704) Fixes #671 --- advanced-creation.md | 7 +- migration-guides.md | 12 +-- readme.md | 113 ++++++++++++++++------ source/as-promise.js | 36 ++++++- source/errors.js | 3 +- source/index.js | 6 +- source/normalize-arguments.js | 40 +------- source/request-as-event-emitter.js | 45 +++++++-- test/arguments.js | 9 +- test/create.js | 25 ++--- test/error.js | 24 ++--- test/headers.js | 39 ++++---- test/hooks.js | 18 ++-- test/http.js | 2 +- test/merge-instances.js | 8 +- test/post.js | 24 ++--- test/promise.js | 4 +- test/{json-parse.js => response-parse.js} | 52 +++++++--- test/stream.js | 4 +- 19 files changed, 272 insertions(+), 199 deletions(-) rename test/{json-parse.js => response-parse.js} (50%) diff --git a/advanced-creation.md b/advanced-creation.md index a03bce256..97ca9a642 100644 --- a/advanced-creation.md +++ b/advanced-creation.md @@ -54,7 +54,7 @@ const settings = { return next(options); }, options: got.mergeOptions(got.defaults.options, { - json: true + responseType: 'json' }) }; @@ -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 }; diff --git a/migration-guides.md b/migration-guides.md index 4f05b90ec..7168d7d56 100644 --- a/migration-guides.md +++ b/migration-guides.md @@ -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) @@ -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. @@ -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; diff --git a/readme.md b/readme.md index c0553c416..9890dee8c 100644 --- a/readme.md +++ b/readme.md @@ -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) @@ -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`
+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`
+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) @@ -182,25 +212,13 @@ Default: `'utf8'` ###### form -Type: `boolean`
-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`
-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 @@ -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[]`
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[]`
@@ -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'); @@ -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. @@ -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' } @@ -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 @@ -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' }); ``` @@ -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`. @@ -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)` } @@ -1094,7 +1147,7 @@ const h2got = got.extend({request}); | Advanced timeouts | ✔ | ✖ | ✖ | ✖ | ✖ | | Timings | ✔ | ✔ | ✖ | ✖ | ✖ | | Errors with metadata | ✔ | ✖ | ✖ | ✔ | ✖ | -| JSON mode | ✔ | ✔ | ✖ | ✔ | ✔ | +| JSON mode | ✔ | ✔ | ✔ | ✔ | ✔ | | Custom defaults | ✔ | ✔ | ✖ | ✔ | ✖ | | Composable | ✔ | ✖ | ✖ | ✖ | ✔ | | Hooks | ✔ | ✖ | ✖ | ✔ | ✖ | diff --git a/source/as-promise.js b/source/as-promise.js index c5023253f..5de69324c 100644 --- a/source/as-promise.js +++ b/source/as-promise.js @@ -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); @@ -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); @@ -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); @@ -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; }; diff --git a/source/errors.js b/source/errors.js index b6cbadc3c..74a42343a 100644 --- a/source/errors.js +++ b/source/errors.js @@ -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]; } diff --git a/source/index.js b/source/index.js index cbf7c3739..f13d87440 100644 --- a/source/index.js +++ b/source/index.js @@ -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 }; diff --git a/source/normalize-arguments.js b/source/normalize-arguments.js index 29a1916c7..be461e7ff 100644 --- a/source/normalize-arguments.js +++ b/source/normalize-arguments.js @@ -5,7 +5,6 @@ const is = require('@sindresorhus/is'); const urlParseLax = require('url-parse-lax'); const lowercaseKeys = require('lowercase-keys'); const urlToOptions = require('./utils/url-to-options'); -const isFormData = require('./utils/is-form-data').default; const validateSearchParams = require('./utils/validate-search-params'); const merge = require('./merge'); const knownHookEvents = require('./known-hook-events'); @@ -34,10 +33,6 @@ const preNormalize = (options, defaults) => { options.baseUrl += '/'; } - if (options.stream) { - options.json = false; - } - if (is.nullOrUndefined(options.hooks)) { options.hooks = {}; } else if (!is.object(options.hooks)) { @@ -201,43 +196,14 @@ const normalize = (url, options, defaults) => { } } - if (options.json && is.undefined(headers.accept)) { - headers.accept = 'application/json'; - } - if (options.decompress && is.undefined(headers['accept-encoding'])) { headers['accept-encoding'] = 'gzip, deflate'; } - const {body} = options; - if (is.nullOrUndefined(body)) { - options.method = options.method ? options.method.toUpperCase() : 'GET'; + if (options.method) { + options.method = options.method.toUpperCase(); } else { - const isObject = is.object(body) && !is.buffer(body) && !is.nodeStream(body); - if (!is.nodeStream(body) && !is.string(body) && !is.buffer(body) && !(options.form || options.json)) { - throw new TypeError('The `body` option must be a stream.Readable, string or Buffer'); - } - - if (options.json && !(isObject || is.array(body))) { - throw new TypeError('The `body` option must be an Object or Array when the `json` option is used'); - } - - if (options.form && !isObject) { - throw new TypeError('The `body` option must be an Object when the `form` option is used'); - } - - if (isFormData(body)) { - // Special case for https://github.com/form-data/form-data - headers['content-type'] = headers['content-type'] || `multipart/form-data; boundary=${body.getBoundary()}`; - } else if (options.form) { - headers['content-type'] = headers['content-type'] || 'application/x-www-form-urlencoded'; - options.body = (new URLSearchParams(body)).toString(); - } else if (options.json) { - headers['content-type'] = headers['content-type'] || 'application/json'; - options.body = JSON.stringify(body); - } - - options.method = options.method ? options.method.toUpperCase() : 'POST'; + options.method = is.nullOrUndefined(options.body) ? 'GET' : 'POST'; } if (!is.function(options.retry.retries)) { diff --git a/source/request-as-event-emitter.js b/source/request-as-event-emitter.js index 79586af84..3e6c2ed9d 100644 --- a/source/request-as-event-emitter.js +++ b/source/request-as-event-emitter.js @@ -1,5 +1,5 @@ 'use strict'; -const {URL} = require('url'); // TODO: Use the `URL` global when targeting Node.js 10 +const {URL, URLSearchParams} = require('url'); // TODO: Use the `URL` global when targeting Node.js 10 const util = require('util'); const EventEmitter = require('events'); const http = require('http'); @@ -11,6 +11,7 @@ const is = require('@sindresorhus/is'); const timer = require('@szmarczak/http-timer'); const timedOut = require('./utils/timed-out'); const getBodySize = require('./utils/get-body-size'); +const isFormData = require('./utils/is-form-data'); const getResponse = require('./get-response'); const progress = require('./progress'); const {CacheError, UnsupportedProtocolError, MaxRedirectsError, RequestError, TimeoutError} = require('./errors'); @@ -280,8 +281,39 @@ module.exports = (options, input) => { setImmediate(async () => { try { + for (const hook of options.hooks.beforeRequest) { + // eslint-disable-next-line no-await-in-loop + await hook(options); + } + + // Serialize body + const {body, headers} = options; + const isForm = !is.nullOrUndefined(options.form); + const isJSON = !is.nullOrUndefined(options.json); + if (!is.nullOrUndefined(body)) { + if (isForm || isJSON) { + throw new TypeError('The `body` option cannot be used with the `json` option or `form` option'); + } + + if (is.object(body) && isFormData(body)) { + // Special case for https://github.com/form-data/form-data + headers['content-type'] = headers['content-type'] || `multipart/form-data; boundary=${body.getBoundary()}`; + } else if (!is.nodeStream(body) && !is.string(body) && !is.buffer(body)) { + throw new TypeError('The `body` option must be a stream.Readable, string, Buffer, Object or Array'); + } + } else if (isForm) { + if (!is.object(options.form)) { + throw new TypeError('The `form` option must be an Object'); + } + + headers['content-type'] = headers['content-type'] || 'application/x-www-form-urlencoded'; + options.body = (new URLSearchParams(options.form)).toString(); + } else if (isJSON) { + headers['content-type'] = headers['content-type'] || 'application/json'; + options.body = JSON.stringify(options.json); + } + // Convert buffer to stream to receive upload progress events (#322) - const {body} = options; if (is.buffer(body)) { options.body = toReadableStream(body); uploadBodySize = body.length; @@ -289,15 +321,14 @@ module.exports = (options, input) => { uploadBodySize = await getBodySize(options); } - if (is.undefined(options.headers['content-length']) && is.undefined(options.headers['transfer-encoding'])) { + if (is.undefined(headers['content-length']) && is.undefined(headers['transfer-encoding'])) { if ((uploadBodySize > 0 || options.method === 'PUT') && !is.null(uploadBodySize)) { - options.headers['content-length'] = uploadBodySize; + headers['content-length'] = uploadBodySize; } } - for (const hook of options.hooks.beforeRequest) { - // eslint-disable-next-line no-await-in-loop - await hook(options); + if (!options.stream && options.responseType === 'json' && is.undefined(headers.accept)) { + options.headers.accept = 'application/json'; } requestUrl = options.href || (new URL(options.path, urlLib.format(options))).toString(); diff --git a/test/arguments.js b/test/arguments.js index 2ebfd4034..cfb21061a 100644 --- a/test/arguments.js +++ b/test/arguments.js @@ -131,8 +131,8 @@ test('should ignore empty searchParams object', async t => { t.is((await got(`${s.url}/test`, {searchParams: {}})).requestUrl, `${s.url}/test`); }); -test('should throw when body is set to object', async t => { - await t.throwsAsync(got(`${s.url}/`, {body: {}}), TypeError); +test('should throw on invalid type of body', async t => { + await t.throwsAsync(got(`${s.url}/`, {body: false}), TypeError); }); test('WHATWG URL support', async t => { @@ -145,11 +145,6 @@ test('should return streams when using stream option', async t => { t.is(data.toString(), 'ok'); }); -test('should ignore JSON option when using stream option', async t => { - const data = await pEvent(got(`${s.url}/stream`, {stream: true, json: true}), 'data'); - t.is(data.toString(), 'ok'); -}); - test('accepts `url` as an option', async t => { await t.notThrowsAsync(got({url: `${s.url}/test`})); }); diff --git a/test/create.js b/test/create.js index 86ce1a1c7..3e18c69f5 100644 --- a/test/create.js +++ b/test/create.js @@ -22,8 +22,8 @@ test.after('cleanup', async () => { }); test('preserve global defaults', async t => { - const globalHeaders = (await got(s.url, {json: true})).body; - const instanceHeaders = (await got.extend()(s.url, {json: true})).body; + const globalHeaders = await got(s.url).json(); + const instanceHeaders = await got.extend()(s.url).json(); t.deepEqual(instanceHeaders, globalHeaders); }); @@ -33,7 +33,7 @@ test('support instance defaults', async t => { 'user-agent': 'custom-ua-string' } }); - const headers = (await instance(s.url, {json: true})).body; + const headers = await instance(s.url).json(); t.is(headers['user-agent'], 'custom-ua-string'); }); @@ -43,12 +43,11 @@ test('support invocation overrides', async t => { 'user-agent': 'custom-ua-string' } }); - const headers = (await instance(s.url, { - json: true, + const headers = await instance(s.url, { headers: { 'user-agent': 'different-ua-string' } - })).body; + }).json(); t.is(headers['user-agent'], 'different-ua-string'); }); @@ -63,7 +62,7 @@ test('curry previous instance defaults', async t => { 'x-bar': 'bar' } }); - const headers = (await instanceB(s.url, {json: true})).body; + const headers = await instanceB(s.url).json(); t.is(headers['x-foo'], 'foo'); t.is(headers['x-bar'], 'bar'); }); @@ -72,9 +71,7 @@ test('custom headers (extend)', async t => { const options = {headers: {unicorn: 'rainbow'}}; const instance = got.extend(options); - const headers = (await instance(`${s.url}/`, { - json: true - })).body; + const headers = await instance(`${s.url}/`).json(); t.is(headers.unicorn, 'rainbow'); }); @@ -108,9 +105,7 @@ test('create', async t => { return next(options); } }); - const headers = (await instance(s.url, { - json: true - })).body; + const headers = await instance(s.url).json(); t.is(headers.unicorn, 'rainbow'); t.is(headers['user-agent'], undefined); }); @@ -127,9 +122,7 @@ test('hooks are merged on got.extend()', t => { test('custom endpoint with custom headers (extend)', async t => { const instance = got.extend({headers: {unicorn: 'rainbow'}, baseUrl: s.url}); - const headers = (await instance('/', { - json: true - })).body; + const headers = await instance('/').json(); t.is(headers.unicorn, 'rainbow'); t.not(headers['user-agent'], undefined); }); diff --git a/test/error.js b/test/error.js index e8a052048..e74d3267c 100644 --- a/test/error.js +++ b/test/error.js @@ -66,30 +66,18 @@ test('dns message', async t => { t.is(error.method, 'GET'); }); -test('options.body error message', async t => { - await t.throwsAsync(got(s.url, {body: {}}), { - message: 'The `body` option must be a stream.Readable, string or Buffer' - }); -}); - -test('options.body json error message', async t => { - await t.throwsAsync(got(s.url, {body: Buffer.from('test'), json: true}), { - message: 'The `body` option must be an Object or Array when the `json` option is used' - }); -}); - test('options.body form error message', async t => { - await t.throwsAsync(got(s.url, {body: Buffer.from('test'), form: true}), { - message: 'The `body` option must be an Object when the `form` option is used' + await t.throwsAsync(got(s.url, {body: Buffer.from('test'), form: ''}), { + message: 'The `body` option cannot be used with the `json` option or `form` option' }); }); -test('no plain object restriction on body', async t => { +test('no plain object restriction on json body', async t => { function CustomObject() { this.a = 123; } - const {body} = await got(`${s.url}/body`, {body: new CustomObject(), json: true}); + const body = await got(`${s.url}/body`, {json: new CustomObject()}).json(); t.deepEqual(body, {a: 123}); }); @@ -186,7 +174,7 @@ test('catch error in mimicResponse', async t => { }); test('errors are thrown directly when options.stream is true', t => { - t.throws(() => got(s.url, {stream: true, body: {}}), { - message: 'The `body` option must be a stream.Readable, string or Buffer' + t.throws(() => got(s.url, {stream: true, hooks: false}), { + message: 'Parameter `hooks` must be an object, not boolean' }); }); diff --git a/test/headers.js b/test/headers.js index 3b6f82f91..87a829fad 100644 --- a/test/headers.js +++ b/test/headers.js @@ -25,22 +25,21 @@ test.after('cleanup', async () => { }); test('user-agent', async t => { - const headers = (await got(s.url, {json: true})).body; + const headers = await got(s.url).json(); t.is(headers['user-agent'], `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`); }); test('accept-encoding', async t => { - const headers = (await got(s.url, {json: true})).body; + const headers = await got(s.url).json(); t.is(headers['accept-encoding'], 'gzip, deflate'); }); test('do not override accept-encoding', async t => { - const headers = (await got(s.url, { - json: true, + const headers = await got(s.url, { headers: { 'accept-encoding': 'gzip' } - })).body; + }).json(); t.is(headers['accept-encoding'], 'gzip'); }); @@ -48,7 +47,7 @@ test('do not remove user headers from `url` object argument', async t => { const headers = (await got({ hostname: s.host, port: s.port, - json: true, + responseType: 'json', protocol: 'http:', headers: { 'X-Request-Id': 'value' @@ -62,28 +61,26 @@ test('do not remove user headers from `url` object argument', async t => { }); test('do not set accept-encoding header when decompress options is false', async t => { - const {body: headers} = await got(s.url, { - json: true, + const headers = await got(s.url, { decompress: false - }); + }).json(); t.false(Reflect.has(headers, 'accept-encoding')); }); test('accept header with json option', async t => { - let headers = (await got(s.url, {json: true})).body; + let headers = await got(s.url).json(); t.is(headers.accept, 'application/json'); - headers = (await got(s.url, { + headers = await got(s.url, { headers: { accept: '' - }, - json: true - })).body; + } + }).json(); t.is(headers.accept, ''); }); test('host', async t => { - const headers = (await got(s.url, {json: true})).body; + const headers = await got(s.url).json(); t.is(headers.host, `localhost:${s.port}`); }); @@ -92,7 +89,7 @@ test('transform names to lowercase', async t => { headers: { 'ACCEPT-ENCODING': 'identity' }, - json: true + responseType: 'json' })).body; t.is(headers['accept-encoding'], 'identity'); }); @@ -197,26 +194,26 @@ test('non-existent headers set to undefined are omitted', async t => { }); test('preserve port in host header if non-standard port', async t => { - const {body} = await got(s.url, {json: true}); + const body = await got(s.url).json(); t.is(body.host, 'localhost:' + s.port); }); test('strip port in host header if explicit standard port (:80) & protocol (HTTP)', async t => { - const {body} = await got('http://httpbin.org:80/headers', {json: true}); + const body = await got('http://httpbin.org:80/headers').json(); t.is(body.headers.Host, 'httpbin.org'); }); test('strip port in host header if explicit standard port (:443) & protocol (HTTPS)', async t => { - const {body} = await got('https://httpbin.org:443/headers', {json: true}); + const body = await got('https://httpbin.org:443/headers').json(); t.is(body.headers.Host, 'httpbin.org'); }); test('strip port in host header if implicit standard port & protocol (HTTP)', async t => { - const {body} = await got('http://httpbin.org/headers', {json: true}); + const body = await got('http://httpbin.org/headers').json(); t.is(body.headers.Host, 'httpbin.org'); }); test('strip port in host header if implicit standard port & protocol (HTTPS)', async t => { - const {body} = await got('https://httpbin.org/headers', {json: true}); + const body = await got('https://httpbin.org/headers').json(); t.is(body.headers.Host, 'httpbin.org'); }); diff --git a/test/hooks.js b/test/hooks.js index ee4b136fc..40f323373 100644 --- a/test/hooks.js +++ b/test/hooks.js @@ -66,7 +66,7 @@ test.after('cleanup', async () => { test('async hooks', async t => { const {body} = await got(s.url, { - json: true, + responseType: 'json', hooks: { beforeRequest: [ async options => { @@ -207,7 +207,7 @@ test('init allows modifications', async t => { test('beforeRequest is called with options', async t => { await got(s.url, { - json: true, + responseType: 'json', hooks: { beforeRequest: [ options => { @@ -221,7 +221,7 @@ test('beforeRequest is called with options', async t => { test('beforeRequest allows modifications', async t => { const {body} = await got(s.url, { - json: true, + responseType: 'json', hooks: { beforeRequest: [ options => { @@ -235,7 +235,7 @@ test('beforeRequest allows modifications', async t => { test('beforeRedirect is called with options', async t => { await got(`${s.url}/redirect`, { - json: true, + responseType: 'json', hooks: { beforeRedirect: [ options => { @@ -249,7 +249,7 @@ test('beforeRedirect is called with options', async t => { test('beforeRedirect allows modifications', async t => { const {body} = await got(`${s.url}/redirect`, { - json: true, + responseType: 'json', hooks: { beforeRedirect: [ options => { @@ -263,7 +263,7 @@ test('beforeRedirect allows modifications', async t => { test('beforeRetry is called with options', async t => { await got(`${s.url}/retry`, { - json: true, + responseType: 'json', retry: 1, throwHttpErrors: false, hooks: { @@ -280,7 +280,7 @@ test('beforeRetry is called with options', async t => { test('beforeRetry allows modifications', async t => { const {body} = await got(`${s.url}/retry`, { - json: true, + responseType: 'json', hooks: { beforeRetry: [ options => { @@ -294,7 +294,7 @@ test('beforeRetry allows modifications', async t => { test('afterResponse is called with response', async t => { await got(`${s.url}`, { - json: true, + responseType: 'json', hooks: { afterResponse: [ response => { @@ -309,7 +309,7 @@ test('afterResponse is called with response', async t => { test('afterResponse allows modifications', async t => { const {body} = await got(`${s.url}`, { - json: true, + responseType: 'json', hooks: { afterResponse: [ response => { diff --git a/test/http.js b/test/http.js index 33ce45ebd..8f529e144 100644 --- a/test/http.js +++ b/test/http.js @@ -69,7 +69,7 @@ test('doesn\'t throw on throwHttpErrors === false', async t => { }); test('invalid protocol throws', async t => { - const error = await t.throwsAsync(got('c:/nope.com', {json: true})); + const error = await t.throwsAsync(got('c:/nope.com').json()); t.is(error.constructor, got.UnsupportedProtocolError); }); diff --git a/test/merge-instances.js b/test/merge-instances.js index ada1f868a..665bb72e0 100644 --- a/test/merge-instances.js +++ b/test/merge-instances.js @@ -24,7 +24,7 @@ test('merging instances', async t => { const instanceB = got.extend({baseUrl: s.url}); const merged = got.mergeInstances(instanceA, instanceB); - const headers = (await merged('/', {json: true})).body; + const headers = await merged('/').json(); t.is(headers.unicorn, 'rainbow'); t.not(headers['user-agent'], undefined); }); @@ -55,7 +55,7 @@ test('merges default handlers & custom handlers', async t => { }); const merged = got.mergeInstances(instanceA, instanceB); - const {body: headers} = await merged(s.url, {json: true}); + const headers = await merged(s.url).json(); t.is(headers.unicorn, 'rainbow'); t.is(headers.cat, 'meow'); }); @@ -69,7 +69,7 @@ test('merging one group & one instance', async t => { const merged = got.mergeInstances(instanceA, instanceB, instanceC); const doubleMerged = got.mergeInstances(merged, instanceD); - const headers = (await doubleMerged(s.url, {json: true})).body; + const headers = await doubleMerged(s.url).json(); t.is(headers.dog, 'woof'); t.is(headers.cat, 'meow'); t.is(headers.bird, 'tweet'); @@ -87,7 +87,7 @@ test('merging two groups of merged instances', async t => { const merged = got.mergeInstances(groupA, groupB); - const headers = (await merged(s.url, {json: true})).body; + const headers = await merged(s.url).json(); t.is(headers.dog, 'woof'); t.is(headers.cat, 'meow'); t.is(headers.bird, 'tweet'); diff --git a/test/post.js b/test/post.js index 7e88c7bd7..9f9e6b80c 100644 --- a/test/post.js +++ b/test/post.js @@ -51,31 +51,29 @@ test('sends Streams', async t => { test('sends plain objects as forms', async t => { const {body} = await got(s.url, { - body: {such: 'wow'}, - form: true + form: {such: 'wow'} }); t.is(body, 'such=wow'); }); test('does NOT support sending arrays as forms', async t => { await t.throwsAsync(got(s.url, { - body: ['such', 'wow'], - form: true + form: ['such', 'wow'] }), TypeError); }); test('sends plain objects as JSON', async t => { const {body} = await got(s.url, { - body: {such: 'wow'}, - json: true + json: {such: 'wow'}, + responseType: 'json' }); t.deepEqual(body, {such: 'wow'}); }); test('sends arrays as JSON', async t => { const {body} = await got(s.url, { - body: ['such', 'wow'], - json: true + json: ['such', 'wow'], + responseType: 'json' }); t.deepEqual(body, ['such', 'wow']); }); @@ -132,18 +130,14 @@ test('content-type header is not overriden when object in options.body', async t headers: { 'content-type': 'doge' }, - body: { + json: { such: 'wow' }, - json: true + responseType: 'json' }); t.is(headers['content-type'], 'doge'); }); -test('throws when json body is not a plain object or array', async t => { - await t.throwsAsync(got(`${s.url}`, {body: '{}', json: true}), TypeError); -}); - test('throws when form body is not a plain object or array', async t => { - await t.throwsAsync(got(`${s.url}`, {body: 'such=wow', form: true}), TypeError); + await t.throwsAsync(got(`${s.url}`, {form: 'such=wow'}), TypeError); }); diff --git a/test/promise.js b/test/promise.js index e02362ced..fa66b6540 100644 --- a/test/promise.js +++ b/test/promise.js @@ -20,13 +20,13 @@ test.after('cleanup', async () => { }); test('should emit request event as promise', async t => { - await got(s.url, {json: true}).on('request', request => { + await got(s.url).json().on('request', request => { t.true(request instanceof ClientRequest); }); }); test('should emit response event as promise', async t => { - await got(s.url, {json: true}).on('response', response => { + await got(s.url).json().on('response', response => { t.true(response instanceof Transform); t.true(response.readable); t.is(response.statusCode, 200); diff --git a/test/json-parse.js b/test/response-parse.js similarity index 50% rename from test/json-parse.js rename to test/response-parse.js index 777912a29..1dff3e2d9 100644 --- a/test/json-parse.js +++ b/test/response-parse.js @@ -4,11 +4,13 @@ import {createServer} from './helpers/server'; let s; +const jsonResponse = '{"data":"dog"}'; + test.before('setup', async () => { s = await createServer(); s.on('/', (request, response) => { - response.end('{"data":"dog"}'); + response.end(jsonResponse); }); s.on('/invalid', (request, response) => { @@ -22,7 +24,7 @@ test.before('setup', async () => { s.on('/non200', (request, response) => { response.statusCode = 500; - response.end('{"data":"dog"}'); + response.end(jsonResponse); }); s.on('/non200-invalid', (request, response) => { @@ -41,42 +43,70 @@ test.after('cleanup', async () => { await s.close(); }); -test('parses response', async t => { - t.deepEqual((await got(s.url, {json: true})).body, {data: 'dog'}); +test('options.resolveBodyOnly works', async t => { + t.deepEqual(await got(s.url, {responseType: 'json', resolveBodyOnly: true}), {data: 'dog'}); +}); + +test('JSON response', async t => { + t.deepEqual((await got(s.url, {responseType: 'json'})).body, {data: 'dog'}); +}); + +test('Buffer response', async t => { + t.deepEqual((await got(s.url, {responseType: 'buffer'})).body, Buffer.from(jsonResponse)); +}); + +test('Text response', async t => { + t.is((await got(s.url, {responseType: 'text'})).body, jsonResponse); +}); + +test('JSON response - promise.json()', async t => { + t.deepEqual(await got(s.url).json(), {data: 'dog'}); +}); + +test('Buffer response - promise.buffer()', async t => { + t.deepEqual(await got(s.url).buffer(), Buffer.from(jsonResponse)); +}); + +test('Text response - promise.text()', async t => { + t.is(await got(s.url).text(), jsonResponse); +}); + +test('throws an error on invalid response type', async t => { + await t.throwsAsync(() => got(s.url, {responseType: 'invalid'}), /^Failed to parse body of type 'invalid'/); }); -test('not parses responses without a body', async t => { - const {body} = await got(`${s.url}/no-body`, {json: true}); +test('doesn\'t parse responses without a body', async t => { + const body = await got(`${s.url}/no-body`).json(); t.is(body, ''); }); test('wraps parsing errors', async t => { - const error = await t.throwsAsync(got(`${s.url}/invalid`, {json: true})); + const error = await t.throwsAsync(got(`${s.url}/invalid`, {responseType: 'json'})); t.regex(error.message, /Unexpected token/); t.true(error.message.includes(error.hostname), error.message); t.is(error.path, '/invalid'); }); test('parses non-200 responses', async t => { - const error = await t.throwsAsync(got(`${s.url}/non200`, {json: true})); + const error = await t.throwsAsync(got(`${s.url}/non200`, {responseType: 'json'})); t.deepEqual(error.response.body, {data: 'dog'}); }); test('ignores errors on invalid non-200 responses', async t => { - const error = await t.throwsAsync(got(`${s.url}/non200-invalid`, {json: true})); + const error = await t.throwsAsync(got(`${s.url}/non200-invalid`, {responseType: 'json'})); t.is(error.message, 'Response code 500 (Internal Server Error)'); t.is(error.response.body, 'Internal error'); t.is(error.path, '/non200-invalid'); }); test('should have statusCode in error', async t => { - const error = await t.throwsAsync(got(`${s.url}/invalid`, {json: true})); + const error = await t.throwsAsync(got(`${s.url}/invalid`, {responseType: 'json'})); t.is(error.constructor, got.ParseError); t.is(error.statusCode, 200); }); test('should set correct headers', async t => { - const {body: headers} = await got(`${s.url}/headers`, {json: true, body: {}}); + const {body: headers} = await got(`${s.url}/headers`, {responseType: 'json', json: {}}); t.is(headers['content-type'], 'application/json'); t.is(headers.accept, 'application/json'); }); diff --git a/test/stream.js b/test/stream.js index 34c503332..1fd4e8991 100644 --- a/test/stream.js +++ b/test/stream.js @@ -43,8 +43,8 @@ test.after('cleanup', async () => { await s.close(); }); -test('options.json is ignored', t => { - t.notThrows(() => got.stream(s.url, {json: true})); +test('options.responseType is ignored', t => { + t.notThrows(() => got.stream(s.url, {responseType: 'json'})); }); test('returns readable stream', async t => {