Skip to content

Commit b394a92

Browse files
committed
fix: guard ctx.create() against ended context after client disconnect
Add isEnded() checks in wrapMiddleware and wrapRouteHandler to prevent crashes when middleware or route handlers run after the client disconnects and the original context is already ended.
1 parent 2c3dc1d commit b394a92

File tree

2 files changed

+71
-0
lines changed

2 files changed

+71
-0
lines changed

src/router.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ function isAllowedMethod(method: string): method is Lowercase<HttpMethod> | 'mou
2323
function wrapMiddleware(fn: AppMiddleware, i?: number): AppMiddleware {
2424
const result: AppMiddleware = async (req, res, next) => {
2525
const reqCtx = req.ctx;
26+
if (reqCtx.isEnded()) {
27+
return next();
28+
}
2629
const ctx = reqCtx.create(`${fn.name || `noname-${i}`} middleware`);
2730

2831
let ended = false;
@@ -54,6 +57,9 @@ function wrapRouteHandler(fn: AppRouteHandler, handlerName?: string) {
5457
const handlerNameLocal = handlerName || fn.name || UNNAMED_CONTROLLER;
5558

5659
const handler: AppMiddleware = async (req, res, next) => {
60+
if (req.originalContext.isEnded()) {
61+
return;
62+
}
5763
req.ctx = req.originalContext.create(handlerNameLocal);
5864
if (req.routeInfo.handlerName !== handlerNameLocal) {
5965
if (req.routeInfo.handlerName === UNNAMED_CONTROLLER) {

src/tests/context-lifecycle.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,4 +326,69 @@ describe('Context Lifecycle', () => {
326326
expect(middlewareOriginalCtx!.abortSignal.aborted).toBe(true);
327327
});
328328
});
329+
330+
describe('Client Disconnect Handling', () => {
331+
it('should not throw when middleware runs after client disconnect', async () => {
332+
let slowMiddlewareCalled = false;
333+
334+
const slowMiddleware = async (req: Request, _res: Response, next: NextFunction) => {
335+
slowMiddlewareCalled = true;
336+
// Simulate client disconnect by destroying the socket
337+
req.socket.destroy();
338+
// Wait for the close event handler's setImmediate to end the context
339+
await new Promise<void>((resolve) => {
340+
setImmediate(() => setImmediate(() => setImmediate(resolve)));
341+
});
342+
next();
343+
};
344+
345+
const nodekit = new NodeKit();
346+
const app = new ExpressKit(nodekit, {
347+
'GET /test': {
348+
beforeAuth: [slowMiddleware],
349+
handler: (_req: Request, res: Response) => {
350+
res.json({ok: true});
351+
},
352+
},
353+
});
354+
355+
await request
356+
.agent(app.express)
357+
.get('/test')
358+
.catch(() => {});
359+
360+
expect(slowMiddlewareCalled).toBe(true);
361+
});
362+
363+
it('should not throw when route handler runs after client disconnect', async () => {
364+
const disconnectMiddleware = async (
365+
req: Request,
366+
_res: Response,
367+
next: NextFunction,
368+
) => {
369+
// Simulate client disconnect by destroying the socket
370+
req.socket.destroy();
371+
// Wait for the close event handler's setImmediate to end the context
372+
await new Promise<void>((resolve) => {
373+
setImmediate(() => setImmediate(() => setImmediate(resolve)));
374+
});
375+
next();
376+
};
377+
378+
const nodekit = new NodeKit();
379+
const app = new ExpressKit(nodekit, {
380+
'GET /test': {
381+
beforeAuth: [disconnectMiddleware],
382+
handler: (_req: Request, res: Response) => {
383+
res.json({ok: true});
384+
},
385+
},
386+
});
387+
388+
await request
389+
.agent(app.express)
390+
.get('/test')
391+
.catch(() => {});
392+
});
393+
});
329394
});

0 commit comments

Comments
 (0)