Skip to content

Commit

Permalink
Add more hooks (#640)
Browse files Browse the repository at this point in the history
Fixes #625 
Fixes #634
  • Loading branch information
szmarczak authored and sindresorhus committed Oct 25, 2018
1 parent af341ca commit 325409c
Show file tree
Hide file tree
Showing 7 changed files with 377 additions and 92 deletions.
83 changes: 79 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,7 @@ got('sindresorhus.com', {

###### hooks

Type: `Object<string, Function[]>`<br>
Default: `{beforeRequest: []}`
Type: `Object<string, Function[]>`

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

Expand All @@ -327,11 +326,87 @@ Hooks allow modifications during the request lifecycle. Hook functions may be as
Type: `Function[]`<br>
Default: `[]`

Called with the normalized request 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. 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**: Modifying the `body` is not recommended because the `content-length` header has already been computed and assigned.
**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>
Default: `[]`

Called with [normalized](source/normalize-arguments.js) [request options](#options). Got will make no further changes to the request. This is especially useful when you want to avoid dead sites. Example:

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

got('example.com', {
hooks: {
beforeRedirect: [
options => {
if (options.hostname === 'deadSite') {
options.hostname = 'fallbackSite';
}
}
]
}
});
```

###### hooks.beforeRetry

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

Called with [normalized](source/normalize-arguments.js) [request options](#options), the error and the retry count. Got will make no further changes to the request. This is especially useful when some extra work is required before the next try. Example:

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

got('example.com', {
hooks: {
beforeRetry: [
(options, error, retryCount) => {
if (error.statusCode === 413) { // Payload too large
options.body = getNewBody();
}
}
]
}
});
```

###### hooks.afterResponse

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

Called with [response object](#response). Each function should return the response or updated options. This is especially useful when you want to refresh an access token. Example:

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

got('example.com', {
hooks: {
afterResponse: [
response => {
if (response.statusCode === 401) { // Unauthorized
return {
headers: {
token: getNewToken(); // Refresh the access token
}
};
}

// No changes otherwise
return response;
}
]
}
});
```

#### Response

Expand Down
17 changes: 17 additions & 0 deletions source/as-promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@ module.exports = options => {

response.body = data;

try {
for (const hook of options.hooks.afterResponse) {
// eslint-disable-next-line no-await-in-loop
response = await hook(response);

if (is.plainObject(response)) {
if (emitter.retry(response) === false) {
reject(new Error('Retry limit reached.'));
}
return;
}
}
} catch (error) {
reject(error);
return;
}

if (options.json && response.body) {
try {
response.body = JSON.parse(response.body);
Expand Down
5 changes: 4 additions & 1 deletion source/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ const defaults = {
'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`
},
hooks: {
beforeRequest: []
beforeRequest: [],
beforeRedirect: [],
beforeRetry: [],
afterResponse: []
},
decompress: true,
throwHttpErrors: true,
Expand Down
7 changes: 6 additions & 1 deletion source/known-hook-events.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
'use strict';

module.exports = ['beforeRequest'];
module.exports = [
'beforeRequest',
'beforeRedirect',
'beforeRetry',
'afterResponse'
];
36 changes: 21 additions & 15 deletions source/normalize-arguments.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const preNormalize = (options, defaults) => {
options.baseUrl += '/';
}

if (options.stream && options.json) {
if (options.stream) {
options.json = false;
}

Expand Down Expand Up @@ -205,27 +205,33 @@ module.exports = (url, options, defaults) => {
const {retries} = options.retry;

options.retry.retries = (iteration, error) => {
if (iteration > retries || (!isRetryOnNetworkErrorAllowed(error) && (!options.retry.methods.has(error.method) || !options.retry.statusCodes.has(error.statusCode)))) {
if (iteration > retries) {
return 0;
}

if (Reflect.has(error, 'headers') && Reflect.has(error.headers, 'retry-after') && retryAfterStatusCodes.has(error.statusCode)) {
let after = Number(error.headers['retry-after']);
if (is.nan(after)) {
after = Date.parse(error.headers['retry-after']) - Date.now();
} else {
after *= 1000;
}

if (after > options.retry.maxRetryAfter) {
if (error !== null) {
if (!isRetryOnNetworkErrorAllowed(error) && (!options.retry.methods.has(error.method) || !options.retry.statusCodes.has(error.statusCode))) {
return 0;
}

return after;
}
if (Reflect.has(error, 'headers') && Reflect.has(error.headers, 'retry-after') && retryAfterStatusCodes.has(error.statusCode)) {
let after = Number(error.headers['retry-after']);
if (is.nan(after)) {
after = Date.parse(error.headers['retry-after']) - Date.now();
} else {
after *= 1000;
}

if (error.statusCode === 413) {
return 0;
if (after > options.retry.maxRetryAfter) {
return 0;
}

return after;
}

if (error.statusCode === 413) {
return 0;
}
}

const noise = Math.random() * 100;
Expand Down
33 changes: 31 additions & 2 deletions source/request-as-event-emitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ module.exports = (options, input) => {
...urlToOptions(redirectURL)
};

for (const hook of options.hooks.beforeRedirect) {
// eslint-disable-next-line no-await-in-loop
await hook(redirectOpts);
}

emitter.emit('redirect', response, redirectOpts);

await get(redirectOpts);
Expand Down Expand Up @@ -219,6 +224,17 @@ module.exports = (options, input) => {

emitter.retry = error => {
let backoff;

if (is.plainObject(error)) {
// Retry with updated options
options = {
...options,
...error
};

error = null;
}

try {
backoff = options.retry.retries(++retryTries, error);
} catch (error2) {
Expand All @@ -227,8 +243,21 @@ module.exports = (options, input) => {
}

if (backoff) {
retryCount++;
setTimeout(get, backoff, {...options, forceRefresh: true});
const retry = async options => {
try {
for (const hook of options.hooks.beforeRetry) {
// eslint-disable-next-line no-await-in-loop
await hook(options, error, retryCount);
}

retryCount++;
await get(options);
} catch (error) {
emitter.emit('error', error);
}
};

setTimeout(retry, backoff, {...options, forceRefresh: true});
return true;
}

Expand Down
Loading

0 comments on commit 325409c

Please sign in to comment.