Skip to content
Open
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: 7 additions & 1 deletion lib/internal/http2/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -364,8 +364,14 @@ function emit(self, ...args) {
// the block of headers on.
function onSessionHeaders(handle, id, cat, flags, headers, sensitiveHeaders) {
const session = this[kOwner];
if (session.destroyed)
// Session may have been destroyed mid-receive (e.g. session.destroy() from a
// 'stream' handler drained via nextTick inside MakeCallback while nghttp2 is
// still walking the receive buffer). Tear down the C++ stream so subsequent
// DATA frames do not call CallJSOnreadMethod with a missing onread.
if (session.destroyed) {
handle.destroy();
return;
}

const type = session[kType];
session[kUpdateTimer]();
Expand Down
23 changes: 23 additions & 0 deletions src/node_http2.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,19 @@ int Http2Session::OnBeginHeadersCallback(nghttp2_session* handle,
// The common case is that we're creating a new stream. The less likely
// case is that we're receiving a set of trailers
if (!stream) [[likely]] {
// Close() may be deferred while mem_recv is in progress (see
// Http2Session::Close). A 'stream' handler that calls session.destroy()
// runs via nextTick from MakeCallback during that window, so later
// HEADERS in the same receive buffer must not create a C++ stream
// whose JS wrapper (and onread) is never installed.
if (session->is_closing()) {
nghttp2_submit_rst_stream(
session->session(),
NGHTTP2_FLAG_NONE,
id,
NGHTTP2_CANCEL);
return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
}
if (!session->CanAddStream() ||
Http2Stream::New(session, id, frame->headers.cat) == nullptr)
[[unlikely]] {
Expand Down Expand Up @@ -1561,6 +1574,16 @@ void Http2StreamListener::OnStreamRead(ssize_t nread, const uv_buf_t& buf) {
return;
}

// Streams created after a deferred session close may never get a JS
// wrapper (handle.onread is only set in Http2Stream[kInit]). Drop the
// chunk instead of asserting in CallJSOnreadMethod.
Local<Value> onread =
stream->object()
->GetInternalField(StreamBase::kOnReadFunctionField)
.As<Value>();
if (!onread->IsFunction())
return;

Local<ArrayBuffer> ab;
if (session->stream_buf_ab_.IsEmpty()) {
ab = ArrayBuffer::New(env->isolate(),
Expand Down
60 changes: 60 additions & 0 deletions test/parallel/test-http2-session-destroy-stream-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use strict';

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');

const http2 = require('http2');

// Regression test for https://github.com/nodejs/node/issues/64850
//
// Destroying the session from a 'stream' handler runs (via nextTick drained
// from MakeCallback) while nghttp2 is still inside mem_recv. Close is deferred
// for that window; later HEADERS/DATA in the same buffer must not abort with
// Assertion failed: onread->IsFunction().

const STREAMS = 8;
const BODY = Buffer.alloc(2048, 'a');
const ROUNDS = 40;

const server = http2.createServer({
settings: { maxConcurrentStreams: 4 },
});

server.on('session', (session) => session.on('error', () => {}));

server.on('stream', (stream) => {
stream.on('error', () => {});
stream.session.destroy();
});

server.listen(0, '127.0.0.1', common.mustCall(() => {
const port = server.address().port;
const origin = `http://127.0.0.1:${port}`;
let remaining = ROUNDS;

const round = () => {
if (remaining-- <= 0) {
server.close();
return;
}

const session = http2.connect(origin);
session.on('error', () => {});
session.on('close', () => setImmediate(round));

session.on('connect', () => {
for (let i = 0; i < STREAMS; i++) {
const stream = session.request({
':path': `/${i}`,
':method': 'POST',
});
stream.on('error', () => {});
stream.resume();
stream.end(BODY);
}
});
};

round();
}));
Loading