Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/base-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Comment thread
DakEnviy marked this conversation as resolved.
req.originalContext.end();
}
});
});

next();
return;
} catch (error) {
Expand Down
6 changes: 0 additions & 6 deletions src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
}

function wrapMiddleware(fn: AppMiddleware, i?: number): AppMiddleware {
const result: AppMiddleware = async (req, res, next) => {

Check warning on line 24 in src/router.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Expected to return a value at the end of async arrow function
const reqCtx = req.ctx;
// Skip creating child context if parent is already ended (e.g. client disconnected).
// Optional chaining for backward compatibility with nodekit < 2.5.0 (no abortSignal).
Expand All @@ -45,7 +45,7 @@
ctx.fail(error);
req.ctx = reqCtx;
next(error);
return;

Check warning on line 48 in src/router.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Async arrow function expected a return value
}
}
};
Expand Down Expand Up @@ -148,12 +148,6 @@
}
});

res.on('close', () => {
setImmediate(() => {
req.originalContext.end();
});
});

next();
};

Expand Down
32 changes: 32 additions & 0 deletions src/tests/context-lifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -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 '..';
Expand Down Expand Up @@ -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);
});
});
});
});
Loading