Skip to content

Commit b508a20

Browse files
mcollinaaduh95
authored andcommitted
http2: adapt receive deferral for Node.js 24
Node.js 24 does not include the later stream lifecycle changes that the original fix relies on. Preserve its reset ordering, avoid JavaScript callbacks after a deferred session close, and let destroyed streams finish without requesting trailers. Signed-off-by: Matteo Collina <hello@matteocollina.com> Refs: #64166 PR-URL: #65093 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent e000e4c commit b508a20

2 files changed

Lines changed: 75 additions & 5 deletions

File tree

src/node_http2.cc

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,11 @@ int Http2Session::OnBeginHeadersCallback(nghttp2_session* handle,
10741074
int32_t id = GetFrameID(frame);
10751075
Debug(session, "beginning headers for stream %d", id);
10761076

1077+
// Close() can be called by JavaScript from an earlier receive callback.
1078+
// Do not create streams that can no longer be exposed to JavaScript.
1079+
if (session->is_close_pending())
1080+
return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
1081+
10771082
BaseObjectPtr<Http2Stream> stream = session->FindStream(id);
10781083
// The common case is that we're creating a new stream. The less likely
10791084
// case is that we're receiving a set of trailers
@@ -1139,6 +1144,12 @@ int Http2Session::OnFrameReceive(nghttp2_session* handle,
11391144
session->statistics_.frame_count++;
11401145
Debug(session, "complete frame received: type: %d",
11411146
frame->hd.type);
1147+
1148+
// JavaScript may have closed the session from an earlier receive callback.
1149+
// FinishClose() runs after nghttp2_session_mem_recv() returns.
1150+
if (session->is_close_pending())
1151+
return 0;
1152+
11421153
switch (frame->hd.type) {
11431154
case NGHTTP2_DATA:
11441155
return session->HandleDataFrame(frame);
@@ -1405,6 +1416,12 @@ int Http2Session::OnDataChunkReceived(nghttp2_session* handle,
14051416
if (len == 0)
14061417
return 0;
14071418

1419+
// Close() can be called by JavaScript from an earlier receive callback.
1420+
// Ignore the rest of the buffered DATA because its stream may never have
1421+
// been exposed to JavaScript and therefore has no onread callback.
1422+
if (session->is_close_pending())
1423+
return 0;
1424+
14081425
// Notify nghttp2 that we've consumed a chunk of data on the connection
14091426
// so that it can send a WINDOW_UPDATE frame. This is a critical part of
14101427
// the flow control process in http2
@@ -2004,6 +2021,11 @@ uint8_t Http2Session::SendPendingData() {
20042021
if (is_sending())
20052022
return 1;
20062023

2024+
// Do not call `nghttp2_session_mem_send()` while nghttp2 is processing
2025+
// incoming data. Sending may close the stream and free nghttp2 state
2026+
// that is still in use by `nghttp2_session_mem_recv()`.
2027+
if (is_receiving()) return 1;
2028+
20072029
// This is cleared by ClearOutgoing().
20082030
set_sending();
20092031

@@ -2603,12 +2625,17 @@ void Http2Stream::SubmitRstStream(const uint32_t code) {
26032625
// Do not call `nghttp2_session_mem_send()` while nghttp2 is processing
26042626
// incoming data. Sending may close the stream and free nghttp2 state
26052627
// that is still in use by `nghttp2_session_mem_recv()`.
2606-
if (session_->is_receiving() && available_outbound_length_ == 0) {
2607-
if (is_stream_cancel(code)) {
2628+
if (session_->is_receiving()) {
2629+
// These resets must be submitted before the current callback returns.
2630+
// In particular, nghttp2 otherwise replaces ENHANCE_YOUR_CALM with
2631+
// INTERNAL_ERROR when OnHeaderCallback returns a temporal failure.
2632+
if (code == NGHTTP2_ENHANCE_YOUR_CALM ||
2633+
code == NGHTTP2_REFUSED_STREAM) {
2634+
FlushRstStream();
2635+
} else {
2636+
// Let queued DATA, including END_STREAM, be serialized before the reset.
26082637
session_->AddPendingRstStream(id_);
2609-
return;
26102638
}
2611-
FlushRstStream();
26122639
return;
26132640
}
26142641

@@ -2864,7 +2891,9 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
28642891
if (stream->available_outbound_length_ == 0 && !stream->is_writable()) {
28652892
Debug(session, "no more data for stream %d", id);
28662893
*flags |= NGHTTP2_DATA_FLAG_EOF;
2867-
if (stream->has_trailers()) {
2894+
// A deferred Destroy() cannot call back into JavaScript for trailers.
2895+
// Let the DATA frame end the stream instead.
2896+
if (stream->has_trailers() && !stream->is_destroyed()) {
28682897
*flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM;
28692898
stream->OnTrailers();
28702899
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
const fixtures = require('../common/fixtures');
8+
const http2 = require('http2');
9+
10+
// Regression test for closing a session while nghttp2 is processing several
11+
// streams from the same input buffer. No stream created after the close can be
12+
// exposed to JavaScript, so delivering its DATA would call a missing onread.
13+
const server = http2.createSecureServer({
14+
key: fixtures.readKey('agent2-key.pem'),
15+
cert: fixtures.readKey('agent2-cert.pem')
16+
});
17+
18+
server.on('stream', common.mustCallAtLeast((stream) => {
19+
stream.on('error', () => {});
20+
stream.session.destroy();
21+
}, 1));
22+
23+
server.listen(0, common.mustCall(() => {
24+
const client = http2.connect(`https://localhost:${server.address().port}`, {
25+
rejectUnauthorized: false
26+
});
27+
client.on('error', () => {});
28+
client.on('close', common.mustCall(() => server.close()));
29+
30+
client.on('remoteSettings', common.mustCall(() => {
31+
for (let i = 0; i < 8; i++) {
32+
const stream = client.request({
33+
':method': 'POST',
34+
':path': `/${i}`
35+
});
36+
stream.on('error', () => {});
37+
stream.resume();
38+
stream.end(Buffer.alloc(512));
39+
}
40+
}));
41+
}));

0 commit comments

Comments
 (0)