feat: Add beforeTimeout hook#804
Conversation
| for (const hook of ky.#options.hooks.beforeTimeout) { | ||
| // eslint-disable-next-line no-await-in-loop | ||
| timeoutError = await hook(timeoutError); | ||
| } |
There was a problem hiding this comment.
quick observation. This suggests that timeoutError is continuously overwritten by each subsequent hook in beforeTimeout, and as a result, only the error returned by the last beforeTimeout hook will be thrown.
There was a problem hiding this comment.
@samicey
Thank you for your response!
Yes, you are correct. It works as a pipeline where the output of one hook becomes the input of the next.
I intentionally implemented it this way to maintain consistency with ky's existing beforeError hook logic.
If you check the beforeError implementation in Ky.ts, it uses the exact same pattern:
// Existing beforeError logic in Ky.ts
for (const hook of ky.#options.hooks.beforeError) {
error = await hook(error, {retryCount: ky.#retryCount});
}I believe applying the same pattern to beforeTimeout is the most consistent approach for this library.
Do you think there is a better approach for this?
|
I think I would mildly prefer to send |
| timeoutError = await hook(timeoutError); | ||
| } | ||
|
|
||
| throw timeoutError; |
There was a problem hiding this comment.
This can throw undefined pretty easily, if the hook just doesn't return anything, which is bad. Looks like beforeError already has this problem, too.
I think we should check the hook result and overwrite error if the result is an error (of any kind)..
There was a problem hiding this comment.
Regarding the potential undefined issue, I plan to fix it by verifying the hook's return value before assigning it. Here is the implementation:
for (const hook of ky.#options.hooks.beforeTimeout) {
// eslint-disable-next-line no-await-in-loop
const result = await hook(timeoutError);
// Only overwrite if the hook returns a valid Error instance
if (result instanceof Error) {
timeoutError = result;
}
}Does this look good to you?
There was a problem hiding this comment.
@sholladay
Thanks! I see that #829 resolves the main feature request.
Would you be okay with me opening a small follow-up PR to add a defensive guard so we only overwrite the error when the hook returns a valid Error instance? For example:
// change to beforeError (new)
for (const hook of ky.#options.hooks.beforeError) {
// eslint-disable-next-line no-await-in-loop
const result = await hook(error);
if (result instanceof Error) {
error = result;
}
}There was a problem hiding this comment.
@sholladay
Thanks — I took the suggestion and opened a small follow-up PR with the defensive guard: #833. Feedback welcome! 😀
@sholladay However, beforeError is currently typed to strictly receive HTTPError. If we merge them, we'll need to broaden the type definition (e.g., HTTPError | TimeoutError). Which direction should I proceed with? I'll update the PR as soon as you guys decide. |
| // Or modify the error message | ||
| error.message = 'Custom timeout message'; | ||
|
|
||
| return error; |
| ``` | ||
| import ky from 'ky'; | ||
|
|
||
| await ky('[https://example.com](https://example.com)', { |
|
|
||
| export type BeforeErrorHook = (error: HTTPError, state: BeforeErrorState) => HTTPError | Promise<HTTPError>; | ||
|
|
||
| export type BeforeTimeoutHook = (error: TimeoutError) => TimeoutError | Promise<TimeoutError>; |
There was a problem hiding this comment.
Must be exported from index.d.ts too.
|
|
||
| for (const hook of ky.#options.hooks.beforeTimeout) { | ||
| // eslint-disable-next-line no-await-in-loop | ||
| timeoutError = await hook(timeoutError); |
There was a problem hiding this comment.
| timeoutError = await hook(timeoutError); | |
| timeoutError = await hook({error: timeoutError}); |
should be an options-object in case we need to add more parameters in the future.
|
I think it should be in |
@constantly-dev if you look closely, you'll notice that I edited the issue title to specify Let's proceed with |
|
Closing since I believe #829 made this PR obsolete. Feel free to let me know if I missed anything. |
Closes #508
Description
This PR introduces a new hook,
beforeTimeout, which allows users to intercept and handleTimeoutErrors globally.Currently, the
beforeErrorhook only receivesHTTPError, forcing users to wrap every request in atry-catchblock to handle timeouts (e.g., for logging or UI notifications). This PR solves that inconvenience.Implementation Details
Based on the discussion in #508, I have implemented the following:
beforeTimeoutas suggested by @sindresorhus.TimeoutError, allowing modification (e.g., changing the error message).BeforeTimeoutHooktype definition and updatedHooksinterface.Example Usage