Skip to content

Commit 20ba444

Browse files
http2: fix graceful session close
Fix issue where session.close() prematurely destroys the session when response.end() was called with an empty payload while active http2 streams still existed. This change ensures that sessions are closed gracefully only after all http2 streams complete and clients properly receive the GOAWAY frame as per the HTTP/2 spec. Refs: https://nodejs.org/api/http2.html\#http2sessionclosecallback
1 parent 038d829 commit 20ba444

File tree

5 files changed

+77
-2
lines changed

5 files changed

+77
-2
lines changed

Diff for: lib/internal/http2/core.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,7 @@ function setupHandle(socket, type, options) {
10681068
if (typeof options.selectPadding === 'function')
10691069
this[kSelectPadding] = options.selectPadding;
10701070
handle.consume(socket._handle);
1071+
handle.onstreamafterwrite = this[kMaybeDestroy].bind(this, null);
10711072

10721073
this[kHandle] = handle;
10731074
if (this[kNativeFields]) {
@@ -1609,11 +1610,13 @@ class Http2Session extends EventEmitter {
16091610
// * session is closed and there are no more pending or open streams
16101611
[kMaybeDestroy](error) {
16111612
if (error == null) {
1613+
const handle = this[kHandle];
1614+
const hasPendingData = !!handle && handle.hasPendingData();
16121615
const state = this[kState];
16131616
// Do not destroy if we're not closed and there are pending/open streams
16141617
if (!this.closed ||
16151618
state.streams.size > 0 ||
1616-
state.pendingStreams.size > 0) {
1619+
state.pendingStreams.size > 0 || hasPendingData) {
16171620
return;
16181621
}
16191622
}
@@ -3300,7 +3303,7 @@ function socketOnClose() {
33003303
state.streams.forEach((stream) => stream.close(NGHTTP2_CANCEL));
33013304
state.pendingStreams.forEach((stream) => stream.close(NGHTTP2_CANCEL));
33023305
session.close();
3303-
session[kMaybeDestroy](err);
3306+
closeSession(session, NGHTTP2_NO_ERROR, err);
33043307
}
33053308
}
33063309

Diff for: src/env_properties.h

+1
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@
285285
V(onsignal_string, "onsignal") \
286286
V(onunpipe_string, "onunpipe") \
287287
V(onwrite_string, "onwrite") \
288+
V(onstreamafterwrite_string, "onstreamafterwrite") \
288289
V(openssl_error_stack, "opensslErrorStack") \
289290
V(options_string, "options") \
290291
V(order_string, "order") \

Diff for: src/node_http2.cc

+22
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,22 @@ void Http2Stream::EmitStatistics() {
765765
});
766766
}
767767

768+
void Http2Session::HasPendingData(const FunctionCallbackInfo<Value>& args) {
769+
Http2Session* session;
770+
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
771+
args.GetReturnValue().Set(session->HasPendingData());
772+
}
773+
774+
bool Http2Session::HasPendingData() const {
775+
nghttp2_session* session = session_.get();
776+
int want_write = nghttp2_session_want_write(session);
777+
int want_read = nghttp2_session_want_read(session);
778+
if (want_write == 0 && want_read == 0) {
779+
return false;
780+
}
781+
return true;
782+
}
783+
768784
void Http2Session::EmitStatistics() {
769785
if (!HasHttp2Observer(env())) [[likely]] {
770786
return;
@@ -1743,6 +1759,8 @@ void Http2Session::HandleSettingsFrame(const nghttp2_frame* frame) {
17431759
void Http2Session::OnStreamAfterWrite(WriteWrap* w, int status) {
17441760
Debug(this, "write finished with status %d", status);
17451761

1762+
HandleScope scope(env()->isolate());
1763+
MakeCallback(env()->onstreamafterwrite_string(), 0, nullptr);
17461764
CHECK(is_write_in_progress());
17471765
set_write_in_progress(false);
17481766

@@ -1965,6 +1983,8 @@ uint8_t Http2Session::SendPendingData() {
19651983
if (!res.async) {
19661984
set_write_in_progress(false);
19671985
ClearOutgoing(res.err);
1986+
HandleScope scope(env()->isolate());
1987+
MakeCallback(env()->onstreamafterwrite_string(), 0, nullptr);
19681988
}
19691989

19701990
MaybeStopReading();
@@ -3478,6 +3498,8 @@ void Initialize(Local<Object> target,
34783498
SetProtoMethod(isolate, session, "receive", Http2Session::Receive);
34793499
SetProtoMethod(isolate, session, "destroy", Http2Session::Destroy);
34803500
SetProtoMethod(isolate, session, "goaway", Http2Session::Goaway);
3501+
SetProtoMethod(
3502+
isolate, session, "hasPendingData", Http2Session::HasPendingData);
34813503
SetProtoMethod(isolate, session, "settings", Http2Session::Settings);
34823504
SetProtoMethod(isolate, session, "request", Http2Session::Request);
34833505
SetProtoMethod(

Diff for: src/node_http2.h

+2
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,7 @@ class Http2Session : public AsyncWrap,
712712
static void Consume(const v8::FunctionCallbackInfo<v8::Value>& args);
713713
static void Receive(const v8::FunctionCallbackInfo<v8::Value>& args);
714714
static void Destroy(const v8::FunctionCallbackInfo<v8::Value>& args);
715+
static void HasPendingData(const v8::FunctionCallbackInfo<v8::Value>& args);
715716
static void Settings(const v8::FunctionCallbackInfo<v8::Value>& args);
716717
static void Request(const v8::FunctionCallbackInfo<v8::Value>& args);
717718
static void SetNextStreamID(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -735,6 +736,7 @@ class Http2Session : public AsyncWrap,
735736

736737
BaseObjectPtr<Http2Ping> PopPing();
737738
bool AddPing(const uint8_t* data, v8::Local<v8::Function> callback);
739+
bool HasPendingData() const;
738740

739741
BaseObjectPtr<Http2Settings> PopSettings();
740742
bool AddSettings(v8::Local<v8::Function> callback);

Diff for: test/parallel/test-http2-session-graceful-close.js

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
const assert = require('assert');
7+
const h2 = require('http2');
8+
9+
const server = h2.createServer();
10+
let response;
11+
12+
server.on('session', common.mustCall(function(session) {
13+
session.on('stream', common.mustCall(function (stream) {
14+
response.end();
15+
session.close();
16+
}));
17+
session.on('close', common.mustCall(function() {
18+
server.close();
19+
}));
20+
}));
21+
22+
server.listen(0, common.mustCall(function() {
23+
const port = server.address().port;
24+
server.once('request', common.mustCall(function (request, resp) {
25+
response = resp;
26+
}));
27+
28+
const url = `http://localhost:${port}`;
29+
const client = h2.connect(url, common.mustCall(function() {
30+
const headers = {
31+
':path': '/',
32+
':method': 'GET',
33+
':scheme': 'http',
34+
':authority': `localhost:${port}`
35+
};
36+
const request = client.request(headers);
37+
request.on('response', common.mustCall(function(headers) {
38+
assert.strictEqual(headers[':status'], 200);
39+
}, 1));
40+
request.on('end', common.mustCall(function() {
41+
client.close();
42+
}));
43+
request.end();
44+
request.resume();
45+
}));
46+
client.on('goaway', common.mustCallAtLeast(1));
47+
}));

0 commit comments

Comments
 (0)