Skip to content

Commit

Permalink
Add init hook (#683)
Browse files Browse the repository at this point in the history
  • Loading branch information
szmarczak authored and sindresorhus committed Jan 14, 2019
1 parent e2d3602 commit 677d0a4
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 39 deletions.
1 change: 1 addition & 0 deletions advanced-creation.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ const defaults = {
'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`
},
hooks: {
init: [],
beforeRequest: [],
beforeRedirect: [],
beforeRetry: [],
Expand Down
26 changes: 9 additions & 17 deletions migration-guides.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 12 additions & 1 deletion 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
- [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)
Expand Down Expand Up @@ -351,6 +351,17 @@ Type: `Object<string, Function[]>`

Hooks allow modifications during the request lifecycle. Hook functions may be async and are run serially.

###### hooks.init

Type: `Function[]`<br>
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[]`<br>
Expand Down
1 change: 1 addition & 0 deletions source/known-hook-events.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

module.exports = [
'init',
'beforeRequest',
'beforeRedirect',
'beforeRetry',
Expand Down
19 changes: 17 additions & 2 deletions source/normalize-arguments.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down Expand Up @@ -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: () => {
Expand Down
107 changes: 88 additions & 19 deletions test/hooks.js
Original file line number Diff line number Diff line change
@@ -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 '..';

Expand All @@ -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', '/');
Expand Down Expand Up @@ -75,61 +79,124 @@ 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)]
}
}), 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)]
}
}), 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)]
}
}), 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)]
}
}), 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: {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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);
}
]
}
Expand All @@ -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: {
Expand Down

0 comments on commit 677d0a4

Please sign in to comment.