Skip to content

Commit cd0f4c6

Browse files
jasnelladdaleax
authored andcommitted
http2: fix abort when client.destroy inside end event
Backport-PR-URL: #14813 Backport-Reviewed-By: Anna Henningsen <[email protected]> Backport-Reviewed-By: Timothy Gu <[email protected]> PR-URL: #14239 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent 97f622b commit cd0f4c6

8 files changed

+58
-58
lines changed

lib/internal/http2/core.js

100644100755
+35-33
Original file line numberDiff line numberDiff line change
@@ -289,11 +289,9 @@ function onSessionRead(nread, buf, handle) {
289289
_unrefActive(this); // Reset the session timeout timer
290290
_unrefActive(stream); // Reset the stream timeout timer
291291

292-
if (nread >= 0) {
292+
if (nread >= 0 && !stream.destroyed) {
293293
if (!stream.push(buf)) {
294-
assert(this.streamReadStop(id) === undefined,
295-
`HTTP/2 Stream ${id} does not exist. Please report this as ' +
296-
'a bug in Node.js`);
294+
this.streamReadStop(id);
297295
state.reading = false;
298296
}
299297
} else {
@@ -1475,44 +1473,48 @@ class Http2Stream extends Duplex {
14751473
this.once('ready', this._destroy.bind(this, err, callback));
14761474
return;
14771475
}
1478-
debug(`[${sessionName(session[kType])}] destroying stream ${this[kID]}`);
1479-
1480-
// Submit RST-STREAM frame if one hasn't been sent already and the
1481-
// stream hasn't closed normally...
1482-
if (!this[kState].rst) {
1483-
const code =
1484-
err instanceof Error ?
1485-
NGHTTP2_INTERNAL_ERROR : NGHTTP2_NO_ERROR;
1486-
this[kSession].rstStream(this, code);
1487-
}
1488-
1476+
process.nextTick(() => {
1477+
debug(`[${sessionName(session[kType])}] destroying stream ${this[kID]}`);
1478+
1479+
// Submit RST-STREAM frame if one hasn't been sent already and the
1480+
// stream hasn't closed normally...
1481+
if (!this[kState].rst && !session.destroyed) {
1482+
const code =
1483+
err instanceof Error ?
1484+
NGHTTP2_INTERNAL_ERROR : NGHTTP2_NO_ERROR;
1485+
this[kSession].rstStream(this, code);
1486+
}
14891487

1490-
// Remove the close handler on the session
1491-
session.removeListener('close', this[kState].closeHandler);
1488+
// Remove the close handler on the session
1489+
session.removeListener('close', this[kState].closeHandler);
14921490

1493-
// Unenroll the timer
1494-
unenroll(this);
1491+
// Unenroll the timer
1492+
unenroll(this);
14951493

1496-
setImmediate(finishStreamDestroy.bind(this, handle));
1497-
session[kState].streams.delete(this[kID]);
1498-
delete this[kSession];
1494+
setImmediate(finishStreamDestroy.bind(this, handle));
14991495

1500-
// All done
1501-
const rst = this[kState].rst;
1502-
const code = rst ? this[kState].rstCode : NGHTTP2_NO_ERROR;
1503-
if (code !== NGHTTP2_NO_ERROR) {
1504-
const err = new errors.Error('ERR_HTTP2_STREAM_ERROR', code);
1505-
process.nextTick(() => this.emit('error', err));
1506-
}
1507-
process.nextTick(emit.bind(this, 'streamClosed', code));
1508-
debug(`[${sessionName(session[kType])}] stream ${this[kID]} destroyed`);
1509-
callback(err);
1496+
// All done
1497+
const rst = this[kState].rst;
1498+
const code = rst ? this[kState].rstCode : NGHTTP2_NO_ERROR;
1499+
if (code !== NGHTTP2_NO_ERROR) {
1500+
const err = new errors.Error('ERR_HTTP2_STREAM_ERROR', code);
1501+
process.nextTick(() => this.emit('error', err));
1502+
}
1503+
process.nextTick(emit.bind(this, 'streamClosed', code));
1504+
debug(`[${sessionName(session[kType])}] stream ${this[kID]} destroyed`);
1505+
callback(err);
1506+
});
15101507
}
15111508
}
15121509

15131510
function finishStreamDestroy(handle) {
1511+
const id = this[kID];
1512+
const session = this[kSession];
1513+
session[kState].streams.delete(id);
1514+
delete this[kSession];
15141515
if (handle !== undefined)
1515-
handle.destroyStream(this[kID]);
1516+
handle.destroyStream(id);
1517+
this.emit('destroy');
15161518
}
15171519

15181520
function processHeaders(headers) {

src/node_http2.cc

100644100755
File mode changed.

src/node_http2.h

100644100755
+3-3
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,6 @@ class Http2Session : public AsyncWrap,
329329
padding_strategy_ = opts.GetPaddingStrategy();
330330

331331
Init(env->event_loop(), type, *opts);
332-
stream_buf_.AllocateSufficientStorage(kAllocBufferSize);
333332
}
334333

335334
~Http2Session() override {
@@ -456,15 +455,16 @@ class Http2Session : public AsyncWrap,
456455
}
457456

458457
char* stream_alloc() {
459-
return *stream_buf_;
458+
return stream_buf_;
460459
}
461460

462461
private:
463462
StreamBase* stream_;
464463
StreamResource::Callback<StreamResource::AllocCb> prev_alloc_cb_;
465464
StreamResource::Callback<StreamResource::ReadCb> prev_read_cb_;
466465
padding_strategy_type padding_strategy_ = PADDING_STRATEGY_NONE;
467-
MaybeStackBuffer<char, kAllocBufferSize> stream_buf_;
466+
467+
char stream_buf_[kAllocBufferSize];
468468
};
469469

470470
class ExternalHeader :

src/node_http2_core-inl.h

100644100755
+2-5
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,7 @@ inline int Nghttp2Session::Free() {
221221
Nghttp2Session* session =
222222
ContainerOf(&Nghttp2Session::prep_,
223223
reinterpret_cast<uv_prepare_t*>(handle));
224-
225224
session->OnFreeSession();
226-
DEBUG_HTTP2("Nghttp2Session %d: session is free\n",
227-
session->session_type_);
228225
};
229226
uv_close(reinterpret_cast<uv_handle_t*>(&prep_), PrepClose);
230227

@@ -302,9 +299,9 @@ inline void Nghttp2Stream::ResetState(
302299
inline void Nghttp2Stream::Destroy() {
303300
DEBUG_HTTP2("Nghttp2Stream %d: destroying stream\n", id_);
304301
// Do nothing if this stream instance is already destroyed
305-
if (IsDestroyed() || IsDestroying())
302+
if (IsDestroyed())
306303
return;
307-
flags_ |= NGHTTP2_STREAM_DESTROYING;
304+
flags_ |= NGHTTP2_STREAM_DESTROYED;
308305
Nghttp2Session* session = this->session_;
309306

310307
if (session != nullptr) {

src/node_http2_core.h

100644100755
+1-7
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,7 @@ enum nghttp2_stream_flags {
6565
// Stream is closed
6666
NGHTTP2_STREAM_CLOSED = 0x8,
6767
// Stream is destroyed
68-
NGHTTP2_STREAM_DESTROYED = 0x10,
69-
// Stream is being destroyed
70-
NGHTTP2_STREAM_DESTROYING = 0x20
68+
NGHTTP2_STREAM_DESTROYED = 0x10
7169
};
7270

7371

@@ -290,10 +288,6 @@ class Nghttp2Stream {
290288
return (flags_ & NGHTTP2_STREAM_DESTROYED) == NGHTTP2_STREAM_DESTROYED;
291289
}
292290

293-
inline bool IsDestroying() const {
294-
return (flags_ & NGHTTP2_STREAM_DESTROYING) == NGHTTP2_STREAM_DESTROYING;
295-
}
296-
297291
// Queue outbound chunks of data to be sent on this stream
298292
inline int Write(
299293
nghttp2_stream_write_t* req,

test/parallel/test-http2-options-max-reserved-streams.js

100644100755
+11-6
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ server.on('listening', common.mustCall(() => {
5151
const client = h2.connect(`http://localhost:${server.address().port}`,
5252
options);
5353

54+
let remaining = 2;
55+
function maybeClose() {
56+
if (--remaining === 0) {
57+
server.close();
58+
client.destroy();
59+
}
60+
}
61+
5462
const req = client.request({ ':path': '/' });
5563

5664
// Because maxReservedRemoteStream is 1, the stream event
@@ -59,15 +67,12 @@ server.on('listening', common.mustCall(() => {
5967
client.on('stream', common.mustCall((stream) => {
6068
stream.resume();
6169
stream.on('end', common.mustCall());
70+
stream.on('streamClosed', common.mustCall(maybeClose));
6271
}));
6372

6473
req.on('response', common.mustCall());
6574

6675
req.resume();
67-
req.on('end', common.mustCall(() => {
68-
server.close();
69-
client.destroy();
70-
}));
71-
req.end();
72-
76+
req.on('end', common.mustCall());
77+
req.on('streamClosed', common.mustCall(maybeClose));
7378
}));

test/parallel/test-http2-response-splitting.js

100644100755
+2-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ server.listen(0, common.mustCall(() => {
6565
assert.strictEqual(headers.location, undefined);
6666
}));
6767
req.resume();
68-
req.on('end', common.mustCall(maybeClose));
68+
req.on('end', common.mustCall());
69+
req.on('streamClosed', common.mustCall(maybeClose));
6970
}
7071

7172
doTest(str, 'location', str);

test/parallel/test-http2-server-socket-destroy.js

100644100755
+4-3
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ function onStream(stream) {
3636

3737
assert.notStrictEqual(stream.session, undefined);
3838
socket.destroy();
39-
assert.strictEqual(stream.session, undefined);
39+
stream.on('destroy', common.mustCall(() => {
40+
assert.strictEqual(stream.session, undefined);
41+
}));
4042
}
4143

4244
server.listen(0);
@@ -49,9 +51,8 @@ server.on('listening', common.mustCall(() => {
4951
[HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST });
5052

5153
req.on('aborted', common.mustCall());
54+
req.resume();
5255
req.on('end', common.mustCall());
53-
req.on('response', common.mustCall());
54-
req.on('data', common.mustCall());
5556

5657
client.on('close', common.mustCall());
5758
}));

0 commit comments

Comments
 (0)