Skip to content

Commit db1b93a

Browse files
committed
http2: flush final GOAWAY after pending writes
Keep the native close pending when an active socket write prevents the terminal GOAWAY from being serialized. Finish the close after the outgoing buffers become available, preserving the requested error code. Handle synchronous completion and transport errors without retrying writes to a failed transport or losing the socket close notification. Add a deterministic regression for default and custom GOAWAY codes, both inside and outside the receive callback. Fixes: #66832 Assisted-by: Codex Signed-off-by: Emerson Gomes <emerson.gomes@gmail.com>
1 parent e463b48 commit db1b93a

3 files changed

Lines changed: 124 additions & 15 deletions

File tree

‎src/node_http2.cc‎

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -829,19 +829,22 @@ void Http2Session::Close(uint32_t code, bool socket_closed) {
829829
return;
830830
set_closing();
831831

832+
pending_close_code_ = code;
833+
pending_close_socket_closed_ = socket_closed;
834+
832835
// Do not flush GOAWAY from inside nghttp2_session_mem_recv() callbacks.
833836
// ConsumeHTTP2Data() finishes the close once mem_recv returns.
834837
if (is_receiving()) {
835838
set_close_pending();
836-
pending_close_code_ = code;
837-
pending_close_socket_closed_ = socket_closed;
838839
return;
839840
}
840841

841842
FinishClose(code, socket_closed);
842843
}
843844

844-
void Http2Session::FinishClose(uint32_t code, bool socket_closed) {
845+
void Http2Session::FinishClose(uint32_t code,
846+
bool socket_closed,
847+
int write_status) {
845848
CHECK(is_closing());
846849

847850
// Stop reading on the i/o stream
@@ -850,15 +853,20 @@ void Http2Session::FinishClose(uint32_t code, bool socket_closed) {
850853
stream_->ReadStop();
851854
}
852855

853-
// If the socket is not closed, then attempt to send a closing GOAWAY
856+
// If the socket is still writable, attempt to send a closing GOAWAY
854857
// frame. There is no guarantee that this GOAWAY will be received by
855858
// the peer but the HTTP/2 spec recommends sending it anyway. We'll
856859
// make a best effort.
857-
if (!socket_closed) {
860+
if (!socket_closed && write_status == 0) {
858861
Debug(this, "terminating session with code %d", code);
859862
CHECK_EQ(nghttp2_session_terminate_session(session_.get(), code), 0);
860-
SendPendingData();
861-
} else if (stream_ != nullptr) {
863+
if (SendPendingData() != 0) {
864+
// An earlier write still owns the outgoing buffers. Keep the session
865+
// alive until its completion lets us flush the terminal GOAWAY.
866+
set_close_pending();
867+
return;
868+
}
869+
} else if (socket_closed && stream_ != nullptr) {
862870
// so that the previous listener of the socket, typically, JS code of a
863871
// (tls) socket will be notified of any activity later
864872
stream_->RemoveStreamListener(this);
@@ -893,10 +901,12 @@ void Http2Session::FinishClose(uint32_t code, bool socket_closed) {
893901
EmitStatistics();
894902
}
895903

896-
void Http2Session::MaybeFinishPendingClose() {
897-
if (!is_close_pending() || is_destroyed()) return;
904+
void Http2Session::MaybeFinishPendingClose(int status) {
905+
if (!is_close_pending() || is_destroyed() || is_receiving() || is_sending()) {
906+
return;
907+
}
898908
set_close_pending(false);
899-
FinishClose(pending_close_code_, pending_close_socket_closed_);
909+
FinishClose(pending_close_code_, pending_close_socket_closed_, status);
900910
}
901911

902912
// Locates an existing known stream by ID. nghttp2 has a similar method
@@ -1844,9 +1854,16 @@ void Http2Session::OnStreamAfterWrite(WriteWrap* w, int status) {
18441854
CHECK(is_write_in_progress());
18451855
set_write_in_progress(false);
18461856

1857+
// ClearOutgoing() may synchronously finish a pending close.
1858+
const bool close_pending = is_close_pending();
18471859
// Inform all pending writes about their completion.
18481860
ClearOutgoing(status);
18491861

1862+
if (close_pending || is_close_pending()) {
1863+
MaybeFinishPendingClose(status);
1864+
return;
1865+
}
1866+
18501867
if (is_reading_stopped() &&
18511868
!is_write_in_progress() &&
18521869
nghttp2_session_want_read(session_.get())) {
@@ -1943,7 +1960,7 @@ void Http2Session::ClearOutgoing(int status) {
19431960

19441961
// Now that we've finished sending queued data, if there are any pending
19451962
// RstStreams we should try sending again and then flush them one by one.
1946-
if (!pending_rst_streams_.empty()) {
1963+
if (status == 0 && !pending_rst_streams_.empty()) {
19471964
std::vector<int32_t> current_pending_rst_streams;
19481965
pending_rst_streams_.swap(current_pending_rst_streams);
19491966

@@ -2065,6 +2082,7 @@ uint8_t Http2Session::SendPendingData() {
20652082
set_write_in_progress(false);
20662083
ClearOutgoing(res.err);
20672084
MaybeNotifyGracefulCloseComplete();
2085+
MaybeFinishPendingClose(res.err);
20682086
}
20692087

20702088
MaybeStopReading();

‎src/node_http2.h‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,8 +1001,8 @@ class Http2Session : public AsyncWrap,
10011001
std::vector<uint8_t> outgoing_storage_;
10021002
size_t outgoing_length_ = 0;
10031003
std::vector<int32_t> pending_rst_streams_;
1004-
// Saved arguments for Close() deferred while nghttp2_session_mem_recv()
1005-
// callbacks are active.
1004+
// Saved arguments for Close() deferred while receiving data or waiting for
1005+
// an outgoing write to finish.
10061006
uint32_t pending_close_code_ = NGHTTP2_NO_ERROR;
10071007
bool pending_close_socket_closed_ = false;
10081008
// Count streams that have been rejected while being opened. Exceeding a fixed
@@ -1019,8 +1019,8 @@ class Http2Session : public AsyncWrap,
10191019

10201020
void CopyDataIntoOutgoing(const uint8_t* src, size_t src_length);
10211021
void ClearOutgoing(int status);
1022-
void FinishClose(uint32_t code, bool socket_closed);
1023-
void MaybeFinishPendingClose();
1022+
void FinishClose(uint32_t code, bool socket_closed, int write_status = 0);
1023+
void MaybeFinishPendingClose(int status = 0);
10241024

10251025
void MaybeNotifyGracefulCloseComplete();
10261026

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
// Destroying a session while a socket write is pending must flush the final
8+
// GOAWAY before ending the socket, including when called from a stream handler.
9+
const assert = require('assert');
10+
const http2 = require('http2');
11+
const { Duplex } = require('stream');
12+
const {
13+
HeadersFrame,
14+
SettingsFrame,
15+
kClientMagic,
16+
kFakeRequestHeaders,
17+
} = require('../common/http2');
18+
19+
for (const [code, deferDestroy] of [
20+
[0, false],
21+
[http2.constants.NGHTTP2_REFUSED_STREAM, false],
22+
[0, true],
23+
[http2.constants.NGHTTP2_REFUSED_STREAM, true],
24+
]) {
25+
const output = [];
26+
let pendingWrite;
27+
const socket = new Duplex({
28+
read() {},
29+
write(chunk, encoding, callback) {
30+
output.push(Buffer.from(chunk));
31+
if (pendingWrite === undefined) {
32+
// Hold the initial SETTINGS write until the stream handler destroys
33+
// the session. No timers or large response bodies are needed.
34+
pendingWrite = callback;
35+
setImmediate(() => {
36+
socket.push(new HeadersFrame(1, kFakeRequestHeaders, 0, true).data);
37+
});
38+
} else {
39+
callback();
40+
}
41+
},
42+
});
43+
44+
const session = http2.performServerHandshake(socket);
45+
session.on('stream', common.mustCall((stream) => {
46+
assert.strictEqual(typeof pendingWrite, 'function');
47+
stream.on('close', common.mustCall());
48+
if (code !== 0) {
49+
stream.on('error', common.mustCall((err) => {
50+
assert.strictEqual(err.code, 'ERR_HTTP2_SESSION_ERROR');
51+
}));
52+
}
53+
const destroy = common.mustCall(() => {
54+
session.destroy(code);
55+
assert.strictEqual(session.destroyed, true);
56+
setImmediate(common.mustCall(() => {
57+
const callback = pendingWrite;
58+
pendingWrite = null;
59+
callback();
60+
}));
61+
});
62+
if (deferDestroy)
63+
setImmediate(destroy);
64+
else
65+
destroy();
66+
}));
67+
session.on('close', common.mustCall());
68+
if (code !== 0) {
69+
session.on('error', common.mustCall((err) => {
70+
assert.strictEqual(err.code, 'ERR_HTTP2_SESSION_ERROR');
71+
}));
72+
}
73+
74+
socket.on('finish', common.mustCall(() => {
75+
const data = Buffer.concat(output);
76+
let goaways = 0;
77+
for (let offset = 0; offset < data.length;) {
78+
const length = data.readUIntBE(offset, 3);
79+
assert(data.length >= offset + 9 + length);
80+
if (data[offset + 3] === 7) {
81+
goaways++;
82+
assert.strictEqual(data.readUInt32BE(offset + 9), 1);
83+
assert.strictEqual(data.readUInt32BE(offset + 13), code);
84+
}
85+
offset += 9 + length;
86+
}
87+
assert.strictEqual(goaways, 1);
88+
}));
89+
90+
socket.push(Buffer.concat([kClientMagic, new SettingsFrame().data]));
91+
}

0 commit comments

Comments
 (0)