-
-
Notifications
You must be signed in to change notification settings - Fork 450
feat: Add beforeTimeout hook #804
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
base: main
Are you sure you want to change the base?
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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?
@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; |
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 tab-indentation
| ``` | ||
| import ky from 'ky'; | ||
|
|
||
| await ky('[https://example.com](https://example.com)', { |
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.
typo
|
|
||
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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 |
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