Skip to content

Commit 107756f

Browse files
jstewmonsindresorhus
authored andcommittedJul 14, 2018
Add beforeRequest hook (#516)
1 parent fb5185a commit 107756f

File tree

6 files changed

+212
-32
lines changed

6 files changed

+212
-32
lines changed
 

‎readme.md‎

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,24 @@ Determines if a `got.HTTPError` is thrown for error responses (non-2xx status co
258258

259259
If this is disabled, requests that encounter an error status code will be resolved with the `response` instead of throwing. This may be useful if you are checking for resource availability and are expecting error responses.
260260

261+
###### hooks
262+
263+
Type: `Object<string, Array<Function>>`<br>
264+
Default: `{ beforeRequest: [] }`
265+
266+
Hooks allow modifications during the request lifecycle. Hook functions may be async and are run serially.
267+
268+
###### hooks.beforeRequest
269+
270+
Type: `Array<Function>`<br>
271+
Default: `[]`
272+
273+
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 uses HMAC-signing.
274+
275+
See the [AWS section](#aws) for an example.
276+
277+
**Note**: Modifying the `body` is not recommended because the `content-length` header has already been computed and assigned.
278+
261279
#### Streams
262280

263281
**Note**: Progress events, redirect events and request/response events can also be used with promises.
@@ -626,47 +644,35 @@ got('http://unix:/var/run/docker.sock:/containers/json');
626644
got('unix:/var/run/docker.sock:/containers/json');
627645
```
628646

647+
629648
## AWS
630649

631-
Requests to AWS services need to have their headers signed. This can be accomplished by using the [`aws4`](https://www.npmjs.com/package/aws4) package. This is an example for querying an ["Elasticsearch Service"](https://aws.amazon.com/elasticsearch-service/) host with a signed request.
650+
Requests to AWS services need to have their headers signed. This can be accomplished by using the [`aws4`](https://www.npmjs.com/package/aws4) package. This is an example for querying an ["API Gateway"](https://docs.aws.amazon.com/apigateway/api-reference/signing-requests/) with a signed request.
632651

633652
```js
634-
const url = require('url');
635653
const AWS = require('aws-sdk');
636654
const aws4 = require('aws4');
637655
const got = require('got');
638-
const config = require('./config');
639-
640-
// Reads keys from the environment or `~/.aws/credentials`. Could be a plain object.
641-
const awsConfig = new AWS.Config({ region: config.region });
642656

643-
function request(url, options) {
644-
const awsOpts = {
645-
region: awsConfig.region,
646-
headers: {
647-
accept: 'application/json',
648-
'content-type': 'application/json'
649-
},
650-
method: 'GET',
651-
json: true
652-
};
653-
654-
// We need to parse the URL before passing it to `got` so `aws4` can sign the request
655-
options = {
656-
...url.parse(url),
657-
...awsOpts,
658-
...options
659-
};
660-
661-
aws4.sign(options, awsConfig.credentials);
662-
663-
return got(options);
664-
}
665-
666-
request(`https://${config.host}/production/users/1`);
657+
const credentials = await new AWS.CredentialProviderChain().resolvePromise();
658+
659+
// Create a Got instance to use relative paths and signed requests
660+
const awsClient = got.extend(
661+
{
662+
baseUrl: 'https://<api-id>.execute-api.<api-region>.amazonaws.com/<stage>/',
663+
hooks: {
664+
beforeRequest: [
665+
async options => {
666+
await credentials.getPromise();
667+
aws4.sign(options, credentials);
668+
}
669+
]
670+
}
671+
}
672+
);
667673

668-
request(`https://${config.host}/production/`, {
669-
// All usual `got` options
674+
const response = await awsClient('endpoint/path', {
675+
// Request-specific options
670676
});
671677
```
672678

‎source/index.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ const defaults = {
2323
throwHttpErrors: true,
2424
headers: {
2525
'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`
26+
},
27+
hooks: {
28+
beforeRequest: []
2629
}
2730
}
2831
};

‎source/normalize-arguments.js‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const urlToOptions = require('./url-to-options');
88
const isFormData = require('./is-form-data');
99

1010
const retryAfterStatusCodes = new Set([413, 429, 503]);
11+
const knownHookEvents = ['beforeRequest'];
1112

1213
module.exports = (url, options, defaults) => {
1314
if (Reflect.has(options, 'url') || (is.object(url) && Reflect.has(url, 'url'))) {
@@ -185,5 +186,31 @@ module.exports = (url, options, defaults) => {
185186
delete options.timeout;
186187
}
187188

189+
if (is.nullOrUndefined(options.hooks)) {
190+
options.hooks = {};
191+
}
192+
if (is.object(options.hooks)) {
193+
for (const hookEvent of knownHookEvents) {
194+
const hooks = options.hooks[hookEvent];
195+
if (is.nullOrUndefined(hooks)) {
196+
options.hooks[hookEvent] = [];
197+
} else if (is.array(hooks)) {
198+
hooks.forEach(
199+
(hook, index) => {
200+
if (!is.function_(hook)) {
201+
throw new TypeError(
202+
`Parameter \`hooks.${hookEvent}[${index}]\` must be a function, not ${is(hook)}`
203+
);
204+
}
205+
}
206+
);
207+
} else {
208+
throw new TypeError(`Parameter \`hooks.${hookEvent}\` must be an array, not ${is(hooks)}`);
209+
}
210+
}
211+
} else {
212+
throw new TypeError(`Parameter \`hooks\` must be an object, not ${is(options.hooks)}`);
213+
}
214+
188215
return options;
189216
};

‎source/request-as-event-emitter.js‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,11 @@ module.exports = (options = {}) => {
241241
options.headers['content-length'] = uploadBodySize;
242242
}
243243

244+
for (const hook of options.hooks.beforeRequest) {
245+
// eslint-disable-next-line no-await-in-loop
246+
await hook(options);
247+
}
248+
244249
get(options);
245250
} catch (error) {
246251
emitter.emit('error', error);

‎test/arguments.js‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,40 @@ test('throws TypeError when `url` is passed as an option', async t => {
101101
await t.throws(got({url: 'example.com'}), {instanceOf: TypeError});
102102
});
103103

104+
test('throws TypeError when `hooks` is not an object', async t => {
105+
await t.throws(
106+
() => got(s.url, {hooks: 'not object'}),
107+
{
108+
instanceOf: TypeError,
109+
message: 'Parameter `hooks` must be an object, not string'
110+
}
111+
);
112+
});
113+
114+
test('throws TypeError when known `hooks` value is not an array', async t => {
115+
await t.throws(
116+
() => got(s.url, {hooks: {beforeRequest: {}}}),
117+
{
118+
instanceOf: TypeError,
119+
message: 'Parameter `hooks.beforeRequest` must be an array, not Object'
120+
}
121+
);
122+
});
123+
124+
test('throws TypeError when known `hooks` array item is not a function', async t => {
125+
await t.throws(
126+
() => got(s.url, {hooks: {beforeRequest: [{}]}}),
127+
{
128+
instanceOf: TypeError,
129+
message: 'Parameter `hooks.beforeRequest[0]` must be a function, not Object'
130+
}
131+
);
132+
});
133+
134+
test('allows extra keys in `hooks`', async t => {
135+
await t.notThrows(() => got(`${s.url}/test`, {hooks: {extra: {}}}));
136+
});
137+
104138
test.after('cleanup', async () => {
105139
await s.close();
106140
});

‎test/hooks.js‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import test from 'ava';
2+
import delay from 'delay';
3+
import {createServer} from './helpers/server';
4+
import got from '..';
5+
6+
let s;
7+
8+
test.before('setup', async () => {
9+
s = await createServer();
10+
const echoHeaders = (req, res) => {
11+
res.statusCode = 200;
12+
res.write(JSON.stringify(req.headers));
13+
res.end();
14+
};
15+
s.on('/', echoHeaders);
16+
await s.listen(s.port);
17+
});
18+
19+
test('beforeRequest receives normalized options', async t => {
20+
await got(
21+
s.url,
22+
{
23+
json: true,
24+
hooks: {
25+
beforeRequest: [
26+
options => {
27+
t.is(options.path, '/');
28+
t.is(options.hostname, 'localhost');
29+
}
30+
]
31+
}
32+
}
33+
);
34+
});
35+
36+
test('beforeRequest allows modifications', async t => {
37+
const res = await got(
38+
s.url,
39+
{
40+
json: true,
41+
hooks: {
42+
beforeRequest: [
43+
options => {
44+
options.headers.foo = 'bar';
45+
}
46+
]
47+
}
48+
}
49+
);
50+
t.is(res.body.foo, 'bar');
51+
});
52+
53+
test('beforeRequest awaits async function', async t => {
54+
const res = await got(
55+
s.url,
56+
{
57+
json: true,
58+
hooks: {
59+
beforeRequest: [
60+
async options => {
61+
await delay(100);
62+
options.headers.foo = 'bar';
63+
}
64+
]
65+
}
66+
}
67+
);
68+
t.is(res.body.foo, 'bar');
69+
});
70+
71+
test('beforeRequest rejects when beforeRequest throws', async t => {
72+
await t.throws(
73+
() => got(s.url, {
74+
hooks: {
75+
beforeRequest: [
76+
() => {
77+
throw new Error('oops');
78+
}
79+
]
80+
}
81+
}),
82+
{
83+
instanceOf: Error,
84+
message: 'oops'
85+
}
86+
);
87+
});
88+
89+
test('beforeRequest rejects when beforeRequest rejects', async t => {
90+
await t.throws(
91+
() => got(s.url, {
92+
hooks: {
93+
beforeRequest: [() => Promise.reject(new Error('oops'))]
94+
}
95+
}),
96+
{
97+
instanceOf: Error,
98+
message: 'oops'
99+
}
100+
);
101+
});
102+
103+
test.after('cleanup', async () => {
104+
await s.close();
105+
});

0 commit comments

Comments
 (0)