Skip to content

Commit 13220e2

Browse files
authored
http2: fix async context loss when trailers carry END_STREAM
The previous fix (f67e45e) wrapped header/response event dispatch in reqAsync.runInAsyncScope(), but missed the stream.push(null) call that triggers the 'end' event. When END_STREAM arrives on a trailing HEADERS frame (as gRPC does), the 'end' event fires in the session's async context instead of the request's context. Wrap stream.push(null) at end-of-stream in reqAsync.runInAsyncScope() so that the 'end' event preserves the correct AsyncLocalStorage context. Refs: #55460 Signed-off-by: Orgad Shaneh <orgad.shaneh@audiocodes.com> PR-URL: #63814 Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 6737aa3 commit 13220e2

2 files changed

Lines changed: 51 additions & 37 deletions

File tree

lib/internal/http2/core.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,11 @@ function onSessionHeaders(handle, id, cat, flags, headers, sensitiveHeaders) {
483483
}
484484
}
485485
if (endOfStream) {
486-
stream.push(null);
486+
const reqAsync = stream[kRequestAsyncResource];
487+
if (reqAsync)
488+
reqAsync.runInAsyncScope(() => stream.push(null));
489+
else
490+
stream.push(null);
487491
}
488492
}
489493

test/parallel/test-http2-async-local-storage.js

Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,46 +10,56 @@ const async_hooks = require('async_hooks');
1010
const storage = new async_hooks.AsyncLocalStorage();
1111

1212
const {
13-
HTTP2_HEADER_CONTENT_TYPE,
1413
HTTP2_HEADER_PATH,
1514
HTTP2_HEADER_STATUS,
1615
} = http2.constants;
1716

18-
const server = http2.createServer();
19-
server.on('stream', (stream) => {
20-
stream.respond({
21-
[HTTP2_HEADER_CONTENT_TYPE]: 'text/plain; charset=utf-8',
22-
[HTTP2_HEADER_STATUS]: 200
23-
});
24-
stream.on('error', common.mustNotCall());
17+
function runTest(serverHandler) {
18+
const server = http2.createServer();
19+
server.on('stream', serverHandler);
20+
21+
server.listen(0, common.mustCall(() => {
22+
const client = storage.run({ id: 0 }, () =>
23+
http2.connect(`http://localhost:${server.address().port}`));
24+
25+
let ended = 0;
26+
function doReq(id) {
27+
const req = client.request({ [HTTP2_HEADER_PATH]: '/' });
28+
29+
req.on('response', common.mustCall((headers) => {
30+
assert.strictEqual(headers[HTTP2_HEADER_STATUS], 200);
31+
assert.strictEqual(storage.getStore().id, id);
32+
}));
33+
req.on('data', common.mustCall(() => {
34+
assert.strictEqual(storage.getStore().id, id);
35+
}));
36+
req.on('end', common.mustCall(() => {
37+
assert.strictEqual(storage.getStore().id, id);
38+
if (++ended === 2) {
39+
server.close();
40+
client.close();
41+
}
42+
}));
43+
}
44+
45+
storage.run({ id: 1 }, () => doReq(1));
46+
storage.run({ id: 2 }, () => doReq(2));
47+
}));
48+
}
49+
50+
// Response without trailers: END_STREAM on the DATA frame.
51+
runTest((stream) => {
52+
stream.respond({ [HTTP2_HEADER_STATUS]: 200 });
2553
stream.end('data');
2654
});
2755

28-
server.listen(0, common.mustCall(async () => {
29-
const client = storage.run({ id: 0 }, () => http2.connect(`http://localhost:${server.address().port}`));
30-
31-
async function doReq(id) {
32-
const req = client.request({ [HTTP2_HEADER_PATH]: '/' });
33-
34-
req.on('response', common.mustCall((headers) => {
35-
assert.strictEqual(headers[HTTP2_HEADER_STATUS], 200);
36-
assert.strictEqual(id, storage.getStore().id);
37-
}));
38-
req.on('data', common.mustCall((data) => {
39-
assert.strictEqual(data.toString(), 'data');
40-
assert.strictEqual(id, storage.getStore().id);
41-
}));
42-
req.on('end', common.mustCall(() => {
43-
assert.strictEqual(id, storage.getStore().id);
44-
server.close();
45-
client.close();
46-
}));
47-
}
48-
49-
function doReqWith(id) {
50-
storage.run({ id }, () => doReq(id));
51-
}
52-
53-
doReqWith(1);
54-
doReqWith(2);
55-
}));
56+
// Response with trailers (as gRPC uses): END_STREAM on the trailing HEADERS
57+
// frame. The 'end' event must still run in the request's context.
58+
runTest((stream) => {
59+
stream.respond({ [HTTP2_HEADER_STATUS]: 200 }, { waitForTrailers: true });
60+
stream.on('wantTrailers', () => {
61+
stream.sendTrailers({ 'grpc-status': '0' });
62+
});
63+
stream.write('data');
64+
stream.end();
65+
});

0 commit comments

Comments
 (0)