-
-
Notifications
You must be signed in to change notification settings - Fork 624
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
feat(middleware): Introduce Timeout Middleware #2615
Conversation
src/middleware/timeout/index.ts
Outdated
|
||
export const timeout = ( | ||
duration: number | string, | ||
options: TimeoutOptions = {} |
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.
How about taking exception
as the second arg:
export const timeout = (
duration: number | string,
exception?: HTTPException | ((c: Context) => HTTPException)
)
How to use it (callback pattern):
app.get(
'/',
timeout(1000, (c) => {
return new HTTPException(408, {
message: `Timeout occurred at ${c.req.path}`,
})
}),
async (c) => {
// ...
}
)
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.
Nice. I fixed it, can you see if the type definitions and other perceptions you think match?
Thank you for the PR. I like this! But we have to discuss some points. One is that I've left a comment. Is
|
Hey @usualoma, I want to know your thoughts on this PR. I think it's a good idea! |
@watany-dev Thank you!
|
src/middleware/timeout/index.ts
Outdated
const errorCode = options.code ?? DEFAULT_ERROR_CODE | ||
const ms = typeof duration === 'string' ? parseDuration(duration) : duration | ||
|
||
return async (context, next) => { |
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.
To make it easier for users to debug, it would be better to return a named function like other middleware.
hono/src/middleware/body-limit/index.ts
Line 50 in e9e3c8a
return async function bodyLimit(c, next) { |
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.
thx! I fixed 758a334.
@yusukebe @usualoma |
Thank you. I think I have mostly rewritten it. Here is the revised spec document. Timeout MiddlewareThis middleware enables you to easily manage request timeouts in your application. It allows you to set a maximum duration for requests and optionally define custom error responses if the specified timeout is exceeded. Importimport { Hono } from 'hono';
import { timeout } from 'hono/timeout'; UsageHere's how to use the Timeout Middleware with both default and custom settings: Default Settings: const app = new Hono();
// Applying a 5-second timeout
app.use('/api', timeout(5000));
// Handling a route
app.get('/api/data', async (c) => {
// Your route handler logic
return c.json({ data: 'Your data here' });
}); Custom Timeout Settings: import { HTTPException } from 'hono/http-exception';
// Custom exception factory function
const customTimeoutException = (context) => new HTTPException(408, {
message: `Request timeout after waiting ${context.req.headers.get('Duration')} seconds. Please try again later.`,
});
// for Static Exception Message
// const customTimeoutException = new HTTPException(408, {
// message: 'Operation timed out. Please try again later.'
// });
// Applying a 1-minute timeout with a custom exception
app.use('/api/long-process', timeout(60000, customTimeoutException));
app.get('/api/long-process', async (c) => {
// Simulate a long process
await new Promise(resolve => setTimeout(resolve, 61000));
return c.json({ data: 'This usually takes longer' });
}); Note
app.get('/sse', async (c) => {
let id = 0
let running = true;
let timer: number | undefined
return streamSSE(c, async (stream) => {
timer = setTimeout(() => {
console.log("Stream timeout reached, closing stream");
stream.close();
}, 3000) as unknown as number
stream.onAbort(async () => {
console.log("Client closed connection");
running = false;
clearTimeout(timer);
});
while (running) {
const message = `It is ${new Date().toISOString()}`
await stream.writeSSE({
data: message,
event: 'time-update',
id: String(id++),
})
await stream.sleep(1000)
}
})
}) Middleware ConflictsBe cautious about the order of middleware, especially when using error-handling or other timing-related middleware, as it might affect the behavior of this timeout middleware. |
Awesome! I like it. But I have a problem with the current code. I tried the following example on Cloudflare Workers with Wrangler and Bun: import { Hono } from '../../src'
import { HTTPException } from '../../src/http-exception'
import { timeout } from '../../src/middleware/timeout'
const app = new Hono()
app.get(
'/',
timeout(1000, (c) => {
return new HTTPException(408, {
message: `Timeout occurred at ${c.req.path}`,
})
}),
async (c) => {
await new Promise((resolve) => setTimeout(resolve, 3000))
}
)
export default app It behaves weirdly. See the screencast: Area.mp4It works correctly in the first request. But from the second request, it will not stop until 3 seconds have passed, and it is accessed 3 times. This behavior happens to both Wrangler and Bun. Can you see it? |
I will check. As I was writing the Stream example above, I thought that maybe the clearTimeout is not working correctly. |
Note. |
Thanks @watany-dev By the way. If I don't set the second arg like the following, it works well. const app = new Hono()
app.get('/', timeout(1000), async (c) => {
await new Promise((resolve) => setTimeout(resolve, 3000))
})
export default app |
I believe this is Google Chrome's behaviour for 408 https://groups.google.com/a/chromium.org/g/chromium-dev/c/urswDsm6Pe0/m/JBObkCT0AAAJ According to the RFC, requests can be repeated in status 408, so I think Google Chrome is resending requests accordingly.
|
You are right! I changed the status code Sorry for bothering you (If you paid for Cloudflare only for this purpose, super sorry!). |
@yusukebe |
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.
LGTM!
Hey @watany-dev ! Looks good! Let's goo with this. We'll release it in the next minor version |
@yusukebe |
I'm thinking we should improve the PR template to tell every PR that adding a new feature should add TSDoc/JSDoc (if it's necessary) @yusukebe This way we can slowly migrate codebase to use TSDoc/JSDoc |
I used to use it for personal use, but I see here that there seems to be a demand for it, so I share it with you.
https://github.com/orgs/honojs/discussions/1765
Many cloud environments implement infrastructure-level timeouts, but it is useful if you want to set per-route timeouts on the application side.
Timeout Middleware
This middleware enables you to easily manage request timeouts in your application. It allows you to set a maximum duration for requests and define custom error responses if the specified timeout is exceeded.
Import
Usage
Here's how to use the Timeout Middleware with default and custom settings:
Default Settings:
Custom Timeout Settings:
Understanding Timeouts
The duration for the timeout can be specified in milliseconds or as a string in a human-readable format, such as '1m' for one minute or '10s' for ten seconds. The parseDuration function translates these string values into milliseconds.
Middleware Conflicts
Be cautious about middleware order, especially when using error-handling or other timing-related middleware, as it might affect the behavior of this timeout middleware.
Author should do the followings, if applicable
bun denoify
to generate files for Deno