diff --git a/src/base-middleware.ts b/src/base-middleware.ts index 2db9749..081293c 100644 --- a/src/base-middleware.ts +++ b/src/base-middleware.ts @@ -93,6 +93,14 @@ export function setupBaseMiddleware(ctx: AppContext, expressApp: Express) { req.originalContext.setTag('http.status_code', statusCode); }); + res.on('close', () => { + setImmediate(() => { + if (!req.originalContext.abortSignal?.aborted) { + req.originalContext.end(); + } + }); + }); + next(); return; } catch (error) { diff --git a/src/router.ts b/src/router.ts index dc0637d..030bff6 100644 --- a/src/router.ts +++ b/src/router.ts @@ -148,12 +148,6 @@ export function setupRoutes(ctx: AppContext, expressApp: Express, routes: AppRou } }); - res.on('close', () => { - setImmediate(() => { - req.originalContext.end(); - }); - }); - next(); }; diff --git a/src/tests/context-lifecycle.test.ts b/src/tests/context-lifecycle.test.ts index b2d2b54..54fb3de 100644 --- a/src/tests/context-lifecycle.test.ts +++ b/src/tests/context-lifecycle.test.ts @@ -1,6 +1,8 @@ /* eslint-disable callback-return */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import request from 'supertest'; +import {getEventListeners} from 'node:events'; + import {AppContext, NodeKit} from '@gravity-ui/nodekit'; import {ExpressKit, NextFunction, Request, Response} from '..'; @@ -398,4 +400,34 @@ describe('Context Lifecycle', () => { .catch(() => {}); }); }); + + describe('Bare app.express handler cleanup', () => { + it('should clean up context for handlers registered directly on app.express', async () => { + const nodekit = new NodeKit(); + const app = new ExpressKit(nodekit, {}); + + const contexts: AppContext[] = []; + + app.express.get('/bare', (req, res) => { + contexts.push((req as Request).originalContext); + res.json({ok: true}); + }); + + const baseline = getEventListeners(nodekit.ctx.abortSignal, 'abort').length; + const N = 20; + + const agent = request.agent(app.express); + for (let i = 0; i < N; i++) { + await agent.get('/bare'); + // Wait for setImmediate in 'close' event handler to execute context cleanup + await new Promise((resolve) => setImmediate(resolve)); + } + + expect(getEventListeners(nodekit.ctx.abortSignal, 'abort').length).toBe(baseline); + expect(contexts.length).toBe(N); + contexts.forEach((ctx) => { + expect(ctx.abortSignal?.aborted).toBe(true); + }); + }); + }); });