From 677d0a479bd99c0cbcffdc5847038745dfa72bd1 Mon Sep 17 00:00:00 2001 From: Szymon Marczak <36894700+szmarczak@users.noreply.github.com> Date: Mon, 14 Jan 2019 13:46:33 +0100 Subject: [PATCH] Add `init` hook (#683) --- advanced-creation.md | 1 + migration-guides.md | 26 +++------ readme.md | 13 ++++- source/known-hook-events.js | 1 + source/normalize-arguments.js | 19 +++++- test/hooks.js | 107 ++++++++++++++++++++++++++++------ 6 files changed, 128 insertions(+), 39 deletions(-) diff --git a/advanced-creation.md b/advanced-creation.md index 86de39a98..67a0b6065 100644 --- a/advanced-creation.md +++ b/advanced-creation.md @@ -98,6 +98,7 @@ const defaults = { 'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)` }, hooks: { + init: [], beforeRequest: [], beforeRedirect: [], beforeRetry: [], diff --git a/migration-guides.md b/migration-guides.md index 0c59f31c1..c82de734e 100644 --- a/migration-guides.md +++ b/migration-guides.md @@ -103,31 +103,23 @@ gotInstance(url, options); ```js const gotInstance = got.extend({ hooks: { - beforeRequest: [ + 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) { - // #1 solution (faster) - const newBody = {}; - for (const [key, value] of Object.entries(options.body)) { - let newValue = value; - do { - newValue = options.jsonReplacer(key, newValue); - } while (typeof newValue === 'object'); - - newBody[key] = newValue; - } - options.body = newBody; - - // #2 solution (slower) - options.body = JSON.parse(JSON.stringify(options.body, 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.json && options.jsonReviver) { - response.body = JSON.stringify(JSON.parse(response.body, options.jsonReviver)); + if (options.originalJson && options.jsonReviver) { + 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 ab1ce4447..f4b4e1090 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 - [Errors with metadata](#errors) - [JSON mode](#json) - [WHATWG URL support](#url) -- [Hooks](https://github.com/sindresorhus/got#hooks) +- [Hooks](#hooks) - [Instances with custom defaults](#instances) - [Composable](advanced-creation.md#merging-instances) - [Electron support](#useelectronnet) @@ -351,6 +351,17 @@ Type: `Object` Hooks allow modifications during the request lifecycle. Hook functions may be async and are run serially. +###### hooks.init + +Type: `Function[]`
+Default: `[]` + +Called with plain [request options](#options), right before their normalization. This is especially useful in conjunction with [`got.extend()`](#instances) and [`got.create()`](advanced-creation.md) when the input needs custom handling. + +See the [Request migration guide](migration-guides.md#breaking-changes) for an example. + +**Note**: This hook must be synchronous! + ###### hooks.beforeRequest Type: `Function[]`
diff --git a/source/known-hook-events.js b/source/known-hook-events.js index 2fa0381f5..a991073e6 100644 --- a/source/known-hook-events.js +++ b/source/known-hook-events.js @@ -1,6 +1,7 @@ 'use strict'; module.exports = [ + 'init', 'beforeRequest', 'beforeRedirect', 'beforeRetry', diff --git a/source/normalize-arguments.js b/source/normalize-arguments.js index 2cdd41d8a..665cbceac 100644 --- a/source/normalize-arguments.js +++ b/source/normalize-arguments.js @@ -11,8 +11,15 @@ const knownHookEvents = require('./known-hook-events'); const retryAfterStatusCodes = new Set([413, 429, 503]); -// `preNormalize` handles static things (lowercasing headers; normalizing baseUrl, timeout, retry) -// While `normalize` does `preNormalize` + handles things which need to be reworked when user changes them +// `preNormalize` handles static options (e.g. headers). +// For example, when you create a custom instance and make a request +// with no static changes, they won't be normalized again. +// +// `normalize` operates on dynamic options - they cannot be saved. +// For example, `body` is everytime different per request. +// When it's done normalizing the new options, it performs merge() +// on the prenormalized options and the normalized ones. + const preNormalize = (options, defaults) => { if (is.nullOrUndefined(options.headers)) { options.headers = {}; @@ -126,6 +133,14 @@ const normalize = (url, options, defaults) => { // Override both null/undefined with default protocol options = merge({path: ''}, url, {protocol: url.protocol || 'https:'}, options); + for (const hook of options.hooks.init) { + const called = hook(options); + + if (is.promise(called)) { + throw new TypeError('The `init` hook must be a synchronous function'); + } + } + const {baseUrl} = options; Object.defineProperty(options, 'baseUrl', { set: () => { diff --git a/test/hooks.js b/test/hooks.js index b402a2463..bc1158733 100644 --- a/test/hooks.js +++ b/test/hooks.js @@ -1,5 +1,6 @@ import test from 'ava'; import delay from 'delay'; +import getStream from 'get-stream'; import {createServer} from './helpers/server'; import got from '..'; @@ -18,6 +19,9 @@ test.before('setup', async () => { }; s.on('/', echoHeaders); + s.on('/body', async (request, response) => { + response.end(await getStream(request)); + }); s.on('/redirect', (request, response) => { response.statusCode = 302; response.setHeader('location', '/'); @@ -75,29 +79,65 @@ test('async hooks', async t => { t.is(body.foo, 'bar'); }); -test('catches thrown errors', async t => { +test('catches init thrown errors', async t => { await t.throwsAsync(() => got(s.url, { hooks: { - beforeRequest: [ - () => { - throw error; - } - ] + init: [() => { + throw error; + }] } }), errorString); }); -test('catches promise rejections', async t => { +test('catches beforeRequest thrown errors', async t => { await t.throwsAsync(() => got(s.url, { hooks: { - beforeRequest: [ - () => Promise.reject(error) - ] + beforeRequest: [() => { + throw error; + }] } }), errorString); }); -test('catches beforeRequest errors', async t => { +test('catches beforeRedirect thrown errors', async t => { + await t.throwsAsync(() => got(`${s.url}/redirect`, { + hooks: { + beforeRedirect: [() => { + throw error; + }] + } + }), errorString); +}); + +test('catches beforeRetry thrown errors', async t => { + await t.throwsAsync(() => got(`${s.url}/retry`, { + hooks: { + beforeRetry: [() => { + throw error; + }] + } + }), errorString); +}); + +test('catches afterResponse thrown errors', async t => { + await t.throwsAsync(() => got(s.url, { + hooks: { + afterResponse: [() => { + throw error; + }] + } + }), errorString); +}); + +test('throws a helpful error when passing async function as init hook', async t => { + await t.throwsAsync(() => got(s.url, { + hooks: { + init: [() => Promise.resolve()] + } + }), 'The `init` hook must be a synchronous function'); +}); + +test('catches beforeRequest promise rejections', async t => { await t.throwsAsync(() => got(s.url, { hooks: { beforeRequest: [() => Promise.reject(error)] @@ -105,7 +145,7 @@ test('catches beforeRequest errors', async t => { }), errorString); }); -test('catches beforeRedirect errors', async t => { +test('catches beforeRedirect promise rejections', async t => { await t.throwsAsync(() => got(`${s.url}/redirect`, { hooks: { beforeRedirect: [() => Promise.reject(error)] @@ -113,7 +153,7 @@ test('catches beforeRedirect errors', async t => { }), errorString); }); -test('catches beforeRetry errors', async t => { +test('catches beforeRetry promise rejections', async t => { await t.throwsAsync(() => got(`${s.url}/retry`, { hooks: { beforeRetry: [() => Promise.reject(error)] @@ -121,7 +161,7 @@ test('catches beforeRetry errors', async t => { }), errorString); }); -test('catches afterResponse errors', async t => { +test('catches afterResponse promise rejections', async t => { await t.throwsAsync(() => got(s.url, { hooks: { afterResponse: [() => Promise.reject(error)] @@ -129,7 +169,34 @@ test('catches afterResponse errors', async t => { }), errorString); }); -test('beforeRequest', async t => { +test('init is called with options', async t => { + await got(s.url, { + json: true, + hooks: { + init: [ + options => { + t.is(options.path, '/'); + t.is(options.hostname, 'localhost'); + } + ] + } + }); +}); + +test('init allows modifications', async t => { + const {body} = await got(`${s.url}/body`, { + hooks: { + init: [ + options => { + options.body = 'foobar'; + } + ] + } + }); + t.is(body, 'foobar'); +}); + +test('beforeRequest is called with options', async t => { await got(s.url, { json: true, hooks: { @@ -157,7 +224,7 @@ test('beforeRequest allows modifications', async t => { t.is(body.foo, 'bar'); }); -test('beforeRedirect', async t => { +test('beforeRedirect is called with options', async t => { await got(`${s.url}/redirect`, { json: true, hooks: { @@ -185,15 +252,17 @@ test('beforeRedirect allows modifications', async t => { t.is(body.foo, 'bar'); }); -test('beforeRetry', async t => { +test('beforeRetry is called with options', async t => { await got(`${s.url}/retry`, { json: true, retry: 1, throwHttpErrors: false, hooks: { beforeRetry: [ - options => { + (options, error, retryCount) => { t.is(options.hostname, 'localhost'); + t.truthy(error); + t.true(retryCount >= 1); } ] } @@ -214,7 +283,7 @@ test('beforeRetry allows modifications', async t => { t.is(body.foo, 'bar'); }); -test('afterResponse', async t => { +test('afterResponse is called with response', async t => { await got(`${s.url}`, { json: true, hooks: {