-
-
Notifications
You must be signed in to change notification settings - Fork 916
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add beforeRequest
hook
#516
Conversation
Note: creating Seems good, but I don't see any sense of that. You could just do: const sign = ...;
const instance = got.create({
options: got.defaults.options,
methods: got.defaults.methods,
handler: (url, options, next) => {
options.headers['sign'] = sign(options);
return next(url, options);
}
});
Can you do another PR for that? To make everything more clear :) |
source/request-as-event-emitter.js
Outdated
@@ -228,6 +228,9 @@ module.exports = (options = {}) => { | |||
options.headers['content-length'] = uploadBodySize; | |||
} | |||
|
|||
if (is.function(options.finalize)) { | |||
await options.finalize(options); | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If got.create
wasn't existing, then it'd be very helpful. I think this option is useless.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry, but we are completely misunderstanding each other.
If I understand your suggestion regarding a handler, then a custom handler would run before the default handler.
The entire point of this feature is to create an opportunity to add a signature after the request has been normalized, just before it is sent.
Here's a little test that might illustrate the difference:
test('does something useful', async t => {
const instance = got.create({
baseUrl: `${s.url}/api/`,
options: got.defaults.options,
methods: got.defaults.methods,
handler: (url, options, next) => {
t.is(options.hostname, 'localhost')
t.is(options.path, '/api/?foo=bar')
return next(url, options);
}
});
await instance('?foo=bar');
});
result:
1 test failed
does something useful
/Users/jstewmon/dev/forks/got/test/finalize.js:95
94: handler: (url, options, next) => {
95: t.is(options.hostname, 'example.com')
96: t.is(options.path, '/api/?foo=bar')
Difference:
- undefined
+ 'example.com'
Object.handler (test/finalize.js:95:6)
got (source/create.js:34:20)
test/finalize.js:100:8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for clarifying this out :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be .on('beforeRequest', ...)
. Options may be overridden.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@szmarczak We could handle that while merging options if needed. if beforeRequest
is set and we're merging in options with another beforeRequest
, we just wrap it in a function that calls both.
I don't like events that allow mutation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I considered doing that in assignOptions
, but I decided that it was better to leave that choice to the caller, since I think it is pretty easy:
const got = require('got');
const a = got.extend({beforeRequest: opts => { opts.header.foo = 'bar'; }});
const b = a.extend({beforeRequest: opts => { a.defaults.beforeRequest(opts); opts.header.bar = 'foo'; }});
If the functions are chained, I don't think there's a convenient way to replace the beforeRequest
option if that's the behavior one desires.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we just wrap it in a function that calls both.
@sindresorhus We need to be strict here. Only for beforeRequest
and retries
.
I decided that it was better to leave that choice to the caller, since I think it is pretty easy
It's OK for now. Lots of things is gonna change if #510 gets merged. #510 needs a lot of polishing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took a look at #510 and had the same initial reaction as others - I don't get it. :-)
Personally, I don't see providing merge/extend features as creating a lot of leverage. Since the options are accessible on a got instance, the caller can always use a merge strategy of their choosing to create the configuration for a new client. If it were my choice, I'd probably just provide Object.assign({}, old, new)
semantics for creating new instances.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jstewmon It's not as easy as it seems to be ;) That's for the future: Got plugins.
readme.md
Outdated
@@ -243,6 +243,15 @@ Determines if a `got.HTTPError` is thrown for error responses (non-2xx status co | |||
|
|||
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. | |||
|
|||
###### finalize |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think beforeRequest
would be a clearer name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm ok with that. I chose finalize
to indicate that this is the very last thing that happens before the request is sent.
I'll rename.
finalize
option to ease request signingbeforeRequest
option to ease request signing
test/before-request.js
Outdated
{ | ||
json: true, | ||
beforeRequest: async options => { | ||
return new Promise( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the delay
module (we already have it as a devDependency here).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I didn't see it. Thanks and done!
beforeRequest
option to ease request signingbeforeRequest
option
readme.md
Outdated
Type: `Function`<br> | ||
Default: `undefined` | ||
|
||
Called with the normalized request options just before the request is sent. You can modify the object. 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth noting that changing the body is not recommended, since the content-length has already been set?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes
This is similar in objective to interceptors in axios. It's also a great place to add support for plugins, or hooks. An API like the following would allow us to expand this to different hooks and provide support for multiple hooks at once: const plugin = got.hooks.beforeRequest.add(async req => {
});
got.hooks.beforeRequest.remove(plugin); Hooks here could be things like:
I support the current implementation, but wanted to bring up the alternative above if we want to future proof the implementation. |
I actually ended up coming back to got for this after trying to solve the problem with axios interceptors. :-) axios request interceptors run before their dispatchRequest method, which is where they normalize the request. But, that's not your point... I considered allowing the config setting an array or a function, but I decided that it was best to start the conversation around the feature by making it as simple as possible. If we allow the setting to be an array (without providing a custom interface), I think that might provide better mechanics for anyone wanting to apply a custom merge strategy to the options for a new instance (versus the function wrapping discussed earlier). If it's conceivable that additional hook points would be added in the future, then we might consider having a top-level |
@brandon93s +1. ~~~But how do hooks compare to |
@szmarczak Events are meant as one-way notifications. Hooks let us transform. |
I rebased off master and updated the interface to reflect the discussion about future-proofing the configuration interface for additional hook events. |
readme.md
Outdated
} | ||
|
||
request(`https://${config.host}/production/users/1`); | ||
// Create a Got instance to use relative paths and signed requests |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what I wanted! A real example :) Now I know the advantage of beforeRequest
. It's always before request. No need to merge anything (you could, but you now, if you merge two merged groups the order is not what you wanted, so beforeRequest
does that job 🎉 ) Cool 🦄
readme.md
Outdated
|
||
request(`https://${config.host}/production/`, { | ||
// All usual `got` options | ||
const response = await awsClient('/endpoint/path', { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace:
const response = await awsClient('/endpoint/path', {
// Request-specific options
});
With:
const response = await awsClient('/endpoint/path', options);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@szmarczak I don't think we should add variables that are not defined in the example. Ideally, examples should be copy-paste runnnable. I also think the comment helps make it clearer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay then :)
@@ -241,6 +241,11 @@ module.exports = (options = {}) => { | |||
options.headers['content-length'] = uploadBodySize; | |||
} | |||
|
|||
for (const hook of options.hooks.beforeRequest) { | |||
// eslint-disable-next-line no-await-in-loop |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would do: await hook(options); // eslint-disable-line no-await-in-loop
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@szmarczak Let's try not to nitpick too much. The super minor stuff we can just fix ourselves when merging.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just want to note that we should keep the style of coding. That's all.
@@ -23,6 +23,9 @@ const defaults = { | |||
throwHttpErrors: true, | |||
headers: { | |||
'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)` | |||
}, | |||
hooks: { | |||
beforeRequest: [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just want to note: got.create({options: {}, methods: [], handler: () => {}})
shouldn't throw. It'd be better if hooks were done like @brandon93s has suggested:
An API like the following would allow us to expand this to different hooks and provide support for multiple hooks at once:
const plugin = got.hooks.beforeRequest.add(async req => { }); got.hooks.beforeRequest.remove(plugin);Hooks here could be things like:
- Authentication
- Logging / Debugging
- For example:
chalk()
of network activity in development mode- Etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I strongly disagree. I'll repeat the rationale I provided earlier in the discussion:
If we allow the setting to be an array (without providing a custom interface), I think that might provide better mechanics for anyone wanting to apply a custom merge strategy to the options for a new instance (versus the function wrapping discussed earlier).
Requiring the use of a custom interface adds no value and prohibits advanced configuration scenarios.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we allow the setting to be an array (without providing a custom interface)
OK. But this doesn't change anything.
Requiring the use of a custom interface adds no value and prohibits advanced configuration scenarios.
I disagree. What advanced configuration scenarios does it prohibit?
I think that might provide better mechanics for anyone wanting to apply a custom merge strategy to the options for a new instance (versus the function wrapping discussed earlier).
You're right here.
Let's assume there's got.hooks
and handler receives a new argument: hooks
{
options,
methods,
handler: (options, hooks, next) => ...
}
What about that? Maybe that's just me, probably I'm wrong. I just haven't seen that from your side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW, do we really need to specify empty hooks in the defaults?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer just a simple array too.
@szmarczak Making it just an array enables the user to construct it however they want. They can, for example, order it based on a condition.
Same reason we have:
got(..., {
headers: {foo: true}
});
Instead of:
got.headers.add({foo: true});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see your point now...
I suspect that there is a performance advantage to defining the defaults.
Without the defaults, an object and knownHookEvents.length
arrays will be created for every request, resulting in more work for the garbage collector.
Further, I think that dynamically creating the properties will introduce a new hidden class for every request. So, I think it is best to keep the default defined.
These are certainly micro optimizations, but I don't see a disadvantage to defining the defaults. Do you?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't :) But look: there are other properties that get redefined: options.timeout
, options.followRedirect
, options.retry
and more. I don't think it's a big deal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The array approach does come with the caveat that we can't support addition or removal of hooks from an instance since the options are run through an Object.freeze
. A new instance would have to be created to modify hooks - they must be provided at instantiation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@brandon93s I would consider that a feature. Immutability is good, it prevents a lot of weird bugs.
Do you have any use-case for when you would want to modify hooks after instantiation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, nothing in particular comes to mind. I'm comfortable with how it has been implemented here. I prefer the consistent API over additional methods to maintain hooks.
source/normalize-arguments.js
Outdated
@@ -8,6 +8,7 @@ const urlToOptions = require('./url-to-options'); | |||
const isFormData = require('./is-form-data'); | |||
|
|||
const RETRY_AFTER_STATUS_CODES = new Set([413, 429, 503]); | |||
const knownHookEvents = ['beforeRequest']; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be named KNOWN_HOOK_EVENTS
to keep the consistency of the code style :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw RETRY_AFTER_STATUS_CODES
was chosen recently in #508, but I'm wondering if y'all would like to reconsider that choice and make it retryAfterStatusCodes
as was the alternative suggestion in that discussion.
This is the only place in this package where this naming style is used. I know that uppercase constants is conventional in some languages, but not JS. Further, having two styles for variable names requires a decision to be made about when to use the uppercase style. Is it for all vars declared as const
? It is for const
outside of a function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jstewmon I agree. I feel like I never know when to put it in uppercase. Better to just always use normal camelcase, indeed.
readme.md
Outdated
@@ -258,6 +258,24 @@ Determines if a `got.HTTPError` is thrown for error responses (non-2xx status co | |||
|
|||
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. | |||
|
|||
###### hooks | |||
|
|||
Type: `Object<string, Array<Function>`<br> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is missing >
readme.md
Outdated
const awsClient = got.extend( | ||
{ | ||
baseUrl: 'https://<api-id>.execute-api.<api-region>.amazonaws.com/<stage>/', | ||
beforeRequest: async options => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
beforeRequest
needs to be nested in hooks
readme.md
Outdated
|
||
request(`https://${config.host}/production/`, { | ||
// All usual `got` options | ||
const response = await awsClient('/endpoint/path', { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@szmarczak I don't think we should add variables that are not defined in the example. Ideally, examples should be copy-paste runnnable. I also think the comment helps make it clearer.
@@ -241,6 +241,11 @@ module.exports = (options = {}) => { | |||
options.headers['content-length'] = uploadBodySize; | |||
} | |||
|
|||
for (const hook of options.hooks.beforeRequest) { | |||
// eslint-disable-next-line no-await-in-loop |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@szmarczak Let's try not to nitpick too much. The super minor stuff we can just fix ourselves when merging.
readme.md
Outdated
|
||
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe just Object.freeze
that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a few ways this could be strictly prohibited, but I think this is case where it's best to provide guidance but not prohibit advanced usage. Someone may have a valid reason for modifying the body, so this note is here to hint they they probably need to reassign content-length
if they change body.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. Or maybe just move
right after try
before it calculates the body size?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would violate the expectation that got will make no further changes to the options before the request is sent. ;-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would violate the expectation that got will make no further changes to the options before the request is sent.
But that's only one header... I think that'd be fine :)
@sindresorhus What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If that is a signed header, it will break the whole thing. It's a big deal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, then you're right. You should note that too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated to say that Got will make no further changes to the request before it is sent.
I used the signing feature as an example of why I did not want to move the call before the last attempt to set content-length
, but we shouldn't specifically mention request signing in the docs for beforeRequest
, since it's not tightly coupled to request signing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About changing content-length
header:
If that is a signed header, it will break the whole thing.
Can you note that? People will need to sign that again then.
readme.md
Outdated
@@ -270,7 +270,7 @@ Hooks allow modifications during the request lifecycle. Hook functions may be as | |||
Type: `Array<Function>`<br> | |||
Default: `[]` | |||
|
|||
Called with the normalized request options just before the request is sent. You can modify the object. 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. | |||
Called with the normalized request options just before the request is sent. 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Butter butter. Don't repeat yourself ;)
just before the request is sent
it says that for itself :)
- hooks are required to be Array<Function> - normalize-arguments validates provided hooks and normalizes missing values
This looks great now. Thank you so much for working on this, @jstewmon :) |
This reverts commit 107756f.
This PR build on the leverage of being able to create got instances (thanks @knksmith57!) by adding a
finalize
option, which eases the use of HMAC request signatures, such as AWS V4.I hope the updated example in the readme makes the motivation clear.
I explored implementing this using an event, but it works better if the hook can be an async function (also demonstrated in the readme).
Along the way, I made a couple of tweaks to other areas of the code, which I included here in separate commits. I can pull those off if they're not wanted, but I thought they were small enough that it would just be easier to propose them alongside this new feature.