-
-
Notifications
You must be signed in to change notification settings - Fork 630
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(middleware): Introduce Timeout Middleware (#2615)
* 0.1 * v0.2 * 1.0 * multi runtime * adding duration * fix * interface * fix * only input number * return a named function * setting exception * simply * rename type * adding typesVersions * typo * add JSDoc * denoify
- Loading branch information
1 parent
a8a5fb9
commit 8cb59e4
Showing
6 changed files
with
186 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import type { Context } from '../../context.ts' | ||
import { HTTPException } from '../../http-exception.ts' | ||
import type { MiddlewareHandler } from '../../types.ts' | ||
|
||
export type HTTPExceptionFunction = (context: Context) => HTTPException | ||
|
||
const defaultTimeoutException = new HTTPException(504, { | ||
message: 'Gateway Timeout', | ||
}) | ||
|
||
/** | ||
* Timeout middleware for Hono. | ||
* | ||
* @param {number} duration - The timeout duration in milliseconds. | ||
* @param {HTTPExceptionFunction | HTTPException} [exception=defaultTimeoutException] - The exception to throw when the timeout occurs. Can be a function that returns an HTTPException or an HTTPException object. | ||
* @returns {MiddlewareHandler} The middleware handler function. | ||
* | ||
* @example | ||
* ```ts | ||
* const app = new Hono() | ||
* | ||
* app.use( | ||
* '/long-request', | ||
* timeout(5000) // Set timeout to 5 seconds | ||
* ) | ||
* | ||
* app.get('/long-request', async (c) => { | ||
* await someLongRunningFunction() | ||
* return c.text('Completed within time limit') | ||
* }) | ||
* ``` | ||
*/ | ||
export const timeout = ( | ||
duration: number, | ||
exception: HTTPExceptionFunction | HTTPException = defaultTimeoutException | ||
): MiddlewareHandler => { | ||
return async function timeout(context, next) { | ||
let timer: number | undefined | ||
const timeoutPromise = new Promise<void>((_, reject) => { | ||
timer = setTimeout(() => { | ||
reject(typeof exception === 'function' ? exception(context) : exception) | ||
}, duration) as unknown as number | ||
}) | ||
|
||
try { | ||
await Promise.race([next(), timeoutPromise]) | ||
} finally { | ||
if (timer !== undefined) { | ||
clearTimeout(timer) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import type { Context } from '../../context' | ||
import { Hono } from '../../hono' | ||
import { HTTPException } from '../../http-exception' | ||
import type { HTTPExceptionFunction } from '.' | ||
import { timeout } from '.' | ||
|
||
describe('Timeout API', () => { | ||
const app = new Hono() | ||
|
||
app.use('/slow-endpoint', timeout(1000)) | ||
app.use( | ||
'/slow-endpoint/custom', | ||
timeout( | ||
1100, | ||
() => new HTTPException(408, { message: 'Request timeout. Please try again later.' }) | ||
) | ||
) | ||
const exception500: HTTPExceptionFunction = (context: Context) => | ||
new HTTPException(500, { message: `Internal Server Error at ${context.req.path}` }) | ||
app.use('/slow-endpoint/error', timeout(1200, exception500)) | ||
app.use('/normal-endpoint', timeout(1000)) | ||
|
||
app.get('/slow-endpoint', async (c) => { | ||
await new Promise((resolve) => setTimeout(resolve, 1100)) | ||
return c.text('This should not show up') | ||
}) | ||
|
||
app.get('/slow-endpoint/custom', async (c) => { | ||
await new Promise((resolve) => setTimeout(resolve, 1200)) | ||
return c.text('This should not show up') | ||
}) | ||
|
||
app.get('/slow-endpoint/error', async (c) => { | ||
await new Promise((resolve) => setTimeout(resolve, 1300)) | ||
return c.text('This should not show up') | ||
}) | ||
|
||
app.get('/normal-endpoint', async (c) => { | ||
await new Promise((resolve) => setTimeout(resolve, 900)) | ||
return c.text('This should not show up') | ||
}) | ||
|
||
it('Should trigger default timeout exception', async () => { | ||
const res = await app.request('http://localhost/slow-endpoint') | ||
expect(res).not.toBeNull() | ||
expect(res.status).toBe(504) | ||
expect(await res.text()).toContain('Gateway Timeout') | ||
}) | ||
|
||
it('Should apply custom exception with function', async () => { | ||
const res = await app.request('http://localhost/slow-endpoint/custom') | ||
expect(res).not.toBeNull() | ||
expect(res.status).toBe(408) | ||
expect(await res.text()).toContain('Request timeout. Please try again later.') | ||
}) | ||
|
||
it('Error timeout with custom status code and message', async () => { | ||
const res = await app.request('http://localhost/slow-endpoint/error') | ||
expect(res).not.toBeNull() | ||
expect(res.status).toBe(500) | ||
expect(await res.text()).toContain('Internal Server Error at /slow-endpoint/error') | ||
}) | ||
|
||
it('No Timeout should pass', async () => { | ||
const res = await app.request('http://localhost/normal-endpoint') | ||
expect(res).not.toBeNull() | ||
expect(res.status).toBe(200) | ||
expect(await res.text()).toContain('This should not show up') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import type { Context } from '../../context' | ||
import { HTTPException } from '../../http-exception' | ||
import type { MiddlewareHandler } from '../../types' | ||
|
||
export type HTTPExceptionFunction = (context: Context) => HTTPException | ||
|
||
const defaultTimeoutException = new HTTPException(504, { | ||
message: 'Gateway Timeout', | ||
}) | ||
|
||
/** | ||
* Timeout middleware for Hono. | ||
* | ||
* @param {number} duration - The timeout duration in milliseconds. | ||
* @param {HTTPExceptionFunction | HTTPException} [exception=defaultTimeoutException] - The exception to throw when the timeout occurs. Can be a function that returns an HTTPException or an HTTPException object. | ||
* @returns {MiddlewareHandler} The middleware handler function. | ||
* | ||
* @example | ||
* ```ts | ||
* const app = new Hono() | ||
* | ||
* app.use( | ||
* '/long-request', | ||
* timeout(5000) // Set timeout to 5 seconds | ||
* ) | ||
* | ||
* app.get('/long-request', async (c) => { | ||
* await someLongRunningFunction() | ||
* return c.text('Completed within time limit') | ||
* }) | ||
* ``` | ||
*/ | ||
export const timeout = ( | ||
duration: number, | ||
exception: HTTPExceptionFunction | HTTPException = defaultTimeoutException | ||
): MiddlewareHandler => { | ||
return async function timeout(context, next) { | ||
let timer: number | undefined | ||
const timeoutPromise = new Promise<void>((_, reject) => { | ||
timer = setTimeout(() => { | ||
reject(typeof exception === 'function' ? exception(context) : exception) | ||
}, duration) as unknown as number | ||
}) | ||
|
||
try { | ||
await Promise.race([next(), timeoutPromise]) | ||
} finally { | ||
if (timer !== undefined) { | ||
clearTimeout(timer) | ||
} | ||
} | ||
} | ||
} |