Skip to content

Commit 0a6672f

Browse files
apapirovskiBethGriggs
authored andcommitted
http2: fix several serious bugs
Currently http2 does not properly submit GOAWAY frames when a session is being destroyed. It also doesn't properly handle when the other party severs the connection after sending a GOAWAY frame, even though it should. Edge, IE & Safari are currently unable to handle empty TRAILERS frames despite them being correctly to spec. Instead send an empty DATA frame with END_STREAM flag in those situations. Fix and adjust several flaky and/or incorrect tests. Backport-PR-URL: #22850 PR-URL: #20772 Fixes: #20705 Fixes: #20750 Fixes: #20850 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Rich Trott <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 11a63dd commit 0a6672f

14 files changed

+145
-115
lines changed

doc/api/http2.md

+4
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,10 @@ The `'trailers'` event is emitted when a block of headers associated with
884884
trailing header fields is received. The listener callback is passed the
885885
[HTTP/2 Headers Object][] and flags associated with the headers.
886886

887+
Note that this event might not be emitted if `http2stream.end()` is called
888+
before trailers are received and the incoming data is not being read or
889+
listened for.
890+
887891
```js
888892
stream.on('trailers', (headers, flags) => {
889893
console.log(headers);

lib/internal/http2/core.js

+66-43
Original file line numberDiff line numberDiff line change
@@ -292,20 +292,25 @@ function onStreamClose(code) {
292292
tryClose(stream[kState].fd);
293293

294294
// Defer destroy we actually emit end.
295-
if (stream._readableState.endEmitted || code !== NGHTTP2_NO_ERROR) {
295+
if (!stream.readable || code !== NGHTTP2_NO_ERROR) {
296296
// If errored or ended, we can destroy immediately.
297-
stream[kMaybeDestroy](null, code);
297+
stream[kMaybeDestroy](code);
298298
} else {
299299
// Wait for end to destroy.
300300
stream.on('end', stream[kMaybeDestroy]);
301301
// Push a null so the stream can end whenever the client consumes
302302
// it completely.
303303
stream.push(null);
304-
// If the client hasn't tried to consume the stream and there is no
305-
// resume scheduled (which would indicate they would consume in the future),
306-
// then just dump the incoming data so that the stream can be destroyed.
307-
if (!stream[kState].didRead && !stream._readableState.resumeScheduled)
304+
305+
// If the user hasn't tried to consume the stream (and this is a server
306+
// session) then just dump the incoming data so that the stream can
307+
// be destroyed.
308+
if (stream[kSession][kType] === NGHTTP2_SESSION_SERVER &&
309+
!stream[kState].didRead &&
310+
stream._readableState.flowing === null)
308311
stream.resume();
312+
else
313+
stream.read(0);
309314
}
310315
}
311316

@@ -330,7 +335,7 @@ function onStreamRead(nread, buf) {
330335
`${sessionName(stream[kSession][kType])}]: ending readable.`);
331336

332337
// defer this until we actually emit end
333-
if (stream._readableState.endEmitted) {
338+
if (!stream.readable) {
334339
stream[kMaybeDestroy]();
335340
} else {
336341
stream.on('end', stream[kMaybeDestroy]);
@@ -421,7 +426,7 @@ function onGoawayData(code, lastStreamID, buf) {
421426
// condition on this side of the session that caused the
422427
// shutdown.
423428
session.destroy(new errors.Error('ERR_HTTP2_SESSION_ERROR', code),
424-
{ errorCode: NGHTTP2_NO_ERROR });
429+
NGHTTP2_NO_ERROR);
425430
}
426431
}
427432

@@ -772,6 +777,21 @@ function emitClose(self, error) {
772777
self.emit('close');
773778
}
774779

780+
function finishSessionDestroy(session, error) {
781+
const socket = session[kSocket];
782+
if (!socket.destroyed)
783+
socket.destroy(error);
784+
785+
session[kProxySocket] = undefined;
786+
session[kSocket] = undefined;
787+
session[kHandle] = undefined;
788+
socket[kSession] = undefined;
789+
socket[kServer] = undefined;
790+
791+
// Finally, emit the close and error events (if necessary) on next tick.
792+
process.nextTick(emitClose, session, error);
793+
}
794+
775795
// Upon creation, the Http2Session takes ownership of the socket. The session
776796
// may not be ready to use immediately if the socket is not yet fully connected.
777797
// In that case, the Http2Session will wait for the socket to connect. Once
@@ -828,6 +848,8 @@ class Http2Session extends EventEmitter {
828848

829849
this[kState] = {
830850
flags: SESSION_FLAGS_PENDING,
851+
goawayCode: null,
852+
goawayLastStreamID: null,
831853
streams: new Map(),
832854
pendingStreams: new Set(),
833855
pendingAck: 0,
@@ -1130,25 +1152,13 @@ class Http2Session extends EventEmitter {
11301152
if (handle !== undefined)
11311153
handle.destroy(code, socket.destroyed);
11321154

1133-
// If there is no error, use setImmediate to destroy the socket on the
1155+
// If the socket is alive, use setImmediate to destroy the session on the
11341156
// next iteration of the event loop in order to give data time to transmit.
11351157
// Otherwise, destroy immediately.
1136-
if (!socket.destroyed) {
1137-
if (!error) {
1138-
setImmediate(socket.destroy.bind(socket));
1139-
} else {
1140-
socket.destroy(error);
1141-
}
1142-
}
1143-
1144-
this[kProxySocket] = undefined;
1145-
this[kSocket] = undefined;
1146-
this[kHandle] = undefined;
1147-
socket[kSession] = undefined;
1148-
socket[kServer] = undefined;
1149-
1150-
// Finally, emit the close and error events (if necessary) on next tick.
1151-
process.nextTick(emitClose, this, error);
1158+
if (!socket.destroyed)
1159+
setImmediate(finishSessionDestroy, this, error);
1160+
else
1161+
finishSessionDestroy(this, error);
11521162
}
11531163

11541164
// Closing the session will:
@@ -1422,11 +1432,8 @@ function afterDoStreamWrite(status, handle, req) {
14221432
}
14231433

14241434
function streamOnResume() {
1425-
if (!this.destroyed && !this.pending) {
1426-
if (!this[kState].didRead)
1427-
this[kState].didRead = true;
1435+
if (!this.destroyed)
14281436
this[kHandle].readStart();
1429-
}
14301437
}
14311438

14321439
function streamOnPause() {
@@ -1441,6 +1448,16 @@ function afterShutdown() {
14411448
stream[kMaybeDestroy]();
14421449
}
14431450

1451+
function finishSendTrailers(stream, headersList) {
1452+
stream[kState].flags &= ~STREAM_FLAGS_HAS_TRAILERS;
1453+
1454+
const ret = stream[kHandle].trailers(headersList);
1455+
if (ret < 0)
1456+
stream.destroy(new NghttpError(ret));
1457+
else
1458+
stream[kMaybeDestroy]();
1459+
}
1460+
14441461
function closeStream(stream, code, shouldSubmitRstStream = true) {
14451462
const state = stream[kState];
14461463
state.flags |= STREAM_FLAGS_CLOSED;
@@ -1502,6 +1519,10 @@ class Http2Stream extends Duplex {
15021519
this[kSession] = session;
15031520
session[kState].pendingStreams.add(this);
15041521

1522+
// Allow our logic for determining whether any reads have happened to
1523+
// work in all situations. This is similar to what we do in _http_incoming.
1524+
this._readableState.readingMore = true;
1525+
15051526
this[kState] = {
15061527
didRead: false,
15071528
flags: STREAM_FLAGS_PENDING,
@@ -1510,7 +1531,6 @@ class Http2Stream extends Duplex {
15101531
trailersReady: false
15111532
};
15121533

1513-
this.on('resume', streamOnResume);
15141534
this.on('pause', streamOnPause);
15151535
}
15161536

@@ -1717,6 +1737,10 @@ class Http2Stream extends Duplex {
17171737
this.push(null);
17181738
return;
17191739
}
1740+
if (!this[kState].didRead) {
1741+
this._readableState.readingMore = false;
1742+
this[kState].didRead = true;
1743+
}
17201744
if (!this.pending) {
17211745
streamOnResume.call(this);
17221746
} else {
@@ -1765,13 +1789,8 @@ class Http2Stream extends Duplex {
17651789
throw headersList;
17661790
this[kSentTrailers] = headers;
17671791

1768-
this[kState].flags &= ~STREAM_FLAGS_HAS_TRAILERS;
1769-
1770-
const ret = this[kHandle].trailers(headersList);
1771-
if (ret < 0)
1772-
this.destroy(new NghttpError(ret));
1773-
else
1774-
this[kMaybeDestroy]();
1792+
// Send the trailers in setImmediate so we don't do it on nghttp2 stack.
1793+
setImmediate(finishSendTrailers, this, headersList);
17751794
}
17761795

17771796
get closed() {
@@ -1861,15 +1880,15 @@ class Http2Stream extends Duplex {
18611880
}
18621881
// The Http2Stream can be destroyed if it has closed and if the readable
18631882
// side has received the final chunk.
1864-
[kMaybeDestroy](error, code = NGHTTP2_NO_ERROR) {
1865-
if (error || code !== NGHTTP2_NO_ERROR) {
1866-
this.destroy(error);
1883+
[kMaybeDestroy](code = NGHTTP2_NO_ERROR) {
1884+
if (code !== NGHTTP2_NO_ERROR) {
1885+
this.destroy();
18671886
return;
18681887
}
18691888

18701889
// TODO(mcollina): remove usage of _*State properties
1871-
if (this._writableState.ended && this._writableState.pendingcb === 0) {
1872-
if (this._readableState.ended && this.closed) {
1890+
if (!this.writable) {
1891+
if (!this.readable && this.closed) {
18731892
this.destroy();
18741893
return;
18751894
}
@@ -1882,7 +1901,7 @@ class Http2Stream extends Duplex {
18821901
this[kSession][kType] === NGHTTP2_SESSION_SERVER &&
18831902
!(state.flags & STREAM_FLAGS_HAS_TRAILERS) &&
18841903
!state.didRead &&
1885-
!this._readableState.resumeScheduled) {
1904+
this._readableState.flowing === null) {
18861905
this.close();
18871906
}
18881907
}
@@ -2445,6 +2464,10 @@ Object.defineProperty(Http2Session.prototype, 'setTimeout', setTimeout);
24452464
function socketOnError(error) {
24462465
const session = this[kSession];
24472466
if (session !== undefined) {
2467+
// We can ignore ECONNRESET after GOAWAY was received as there's nothing
2468+
// we can do and the other side is fully within its rights to do so.
2469+
if (error.code === 'ECONNRESET' && session[kState].goawayCode !== null)
2470+
return session.destroy();
24482471
debug(`Http2Session ${sessionName(session[kType])}: socket error [` +
24492472
`${error.message}]`);
24502473
session.destroy(error);

src/node_http2.cc

+33-17
Original file line numberDiff line numberDiff line change
@@ -631,26 +631,28 @@ inline void Http2Session::EmitStatistics() {
631631
void Http2Session::Close(uint32_t code, bool socket_closed) {
632632
DEBUG_HTTP2SESSION(this, "closing session");
633633

634-
if (flags_ & SESSION_STATE_CLOSED)
634+
if (flags_ & SESSION_STATE_CLOSING)
635635
return;
636-
flags_ |= SESSION_STATE_CLOSED;
636+
flags_ |= SESSION_STATE_CLOSING;
637637

638638
// Stop reading on the i/o stream
639639
if (stream_ != nullptr)
640640
stream_->ReadStop();
641641

642642
// If the socket is not closed, then attempt to send a closing GOAWAY
643643
// frame. There is no guarantee that this GOAWAY will be received by
644-
// the peer but the HTTP/2 spec recommends sendinng it anyway. We'll
644+
// the peer but the HTTP/2 spec recommends sending it anyway. We'll
645645
// make a best effort.
646646
if (!socket_closed) {
647-
Http2Scope h2scope(this);
648647
DEBUG_HTTP2SESSION2(this, "terminating session with code %d", code);
649648
CHECK_EQ(nghttp2_session_terminate_session(session_, code), 0);
649+
SendPendingData();
650650
} else {
651651
Unconsume();
652652
}
653653

654+
flags_ |= SESSION_STATE_CLOSED;
655+
654656
// If there are outstanding pings, those will need to be canceled, do
655657
// so on the next iteration of the event loop to avoid calling out into
656658
// javascript since this may be called during garbage collection.
@@ -1387,25 +1389,32 @@ void Http2Session::MaybeScheduleWrite() {
13871389
}
13881390
}
13891391

1392+
void Http2Session::MaybeStopReading() {
1393+
int want_read = nghttp2_session_want_read(session_);
1394+
DEBUG_HTTP2SESSION2(this, "wants read? %d", want_read);
1395+
if (want_read == 0)
1396+
stream_->ReadStop();
1397+
}
1398+
13901399
// Unset the sending state, finish up all current writes, and reset
13911400
// storage for data and metadata that was associated with these writes.
13921401
void Http2Session::ClearOutgoing(int status) {
13931402
CHECK_NE(flags_ & SESSION_STATE_SENDING, 0);
13941403

1404+
flags_ &= ~SESSION_STATE_SENDING;
1405+
13951406
if (outgoing_buffers_.size() > 0) {
13961407
outgoing_storage_.clear();
13971408

1398-
for (const nghttp2_stream_write& wr : outgoing_buffers_) {
1409+
std::vector<nghttp2_stream_write> current_outgoing_buffers_;
1410+
current_outgoing_buffers_.swap(outgoing_buffers_);
1411+
for (const nghttp2_stream_write& wr : current_outgoing_buffers_) {
13991412
WriteWrap* wrap = wr.req_wrap;
14001413
if (wrap != nullptr)
14011414
wrap->Done(status);
14021415
}
1403-
1404-
outgoing_buffers_.clear();
14051416
}
14061417

1407-
flags_ &= ~SESSION_STATE_SENDING;
1408-
14091418
// Now that we've finished sending queued data, if there are any pending
14101419
// RstStreams we should try sending again and then flush them one by one.
14111420
if (pending_rst_streams_.size() > 0) {
@@ -1525,8 +1534,7 @@ uint8_t Http2Session::SendPendingData() {
15251534
req->Dispose();
15261535
}
15271536

1528-
DEBUG_HTTP2SESSION2(this, "wants data in return? %d",
1529-
nghttp2_session_want_read(session_));
1537+
MaybeStopReading();
15301538

15311539
return 0;
15321540
}
@@ -1691,8 +1699,7 @@ void Http2Session::OnStreamReadImpl(ssize_t nread,
16911699
};
16921700
session->MakeCallback(env->error_string(), arraysize(argv), argv);
16931701
} else {
1694-
DEBUG_HTTP2SESSION2(session, "processed %d bytes. wants more? %d", ret,
1695-
nghttp2_session_want_read(**session));
1702+
session->MaybeStopReading();
16961703
}
16971704
}
16981705

@@ -1928,6 +1935,7 @@ void Http2Stream::OnTrailers() {
19281935
HandleScope scope(isolate);
19291936
Local<Context> context = env()->context();
19301937
Context::Scope context_scope(context);
1938+
flags_ &= ~NGHTTP2_STREAM_FLAG_TRAILERS;
19311939
MakeCallback(env()->ontrailers_string(), 0, nullptr);
19321940
}
19331941

@@ -1936,7 +1944,16 @@ int Http2Stream::SubmitTrailers(nghttp2_nv* nva, size_t len) {
19361944
CHECK(!this->IsDestroyed());
19371945
Http2Scope h2scope(this);
19381946
DEBUG_HTTP2STREAM2(this, "sending %d trailers", len);
1939-
int ret = nghttp2_submit_trailer(**session_, id_, nva, len);
1947+
int ret;
1948+
// Sending an empty trailers frame poses problems in Safari, Edge & IE.
1949+
// Instead we can just send an empty data frame with NGHTTP2_FLAG_END_STREAM
1950+
// to indicate that the stream is ready to be closed.
1951+
if (len == 0) {
1952+
Http2Stream::Provider::Stream prov(this, 0);
1953+
ret = nghttp2_submit_data(**session_, NGHTTP2_FLAG_END_STREAM, id_, *prov);
1954+
} else {
1955+
ret = nghttp2_submit_trailer(**session_, id_, nva, len);
1956+
}
19401957
CHECK_NE(ret, NGHTTP2_ERR_NOMEM);
19411958
return ret;
19421959
}
@@ -2562,8 +2579,7 @@ void Http2Stream::Info(const FunctionCallbackInfo<Value>& args) {
25622579

25632580
Headers list(isolate, context, headers);
25642581
args.GetReturnValue().Set(stream->SubmitInfo(*list, list.length()));
2565-
DEBUG_HTTP2STREAM2(stream, "%d informational headers sent",
2566-
headers->Length());
2582+
DEBUG_HTTP2STREAM2(stream, "%d informational headers sent", list.length());
25672583
}
25682584

25692585
// Submits trailing headers on the Http2Stream
@@ -2578,7 +2594,7 @@ void Http2Stream::Trailers(const FunctionCallbackInfo<Value>& args) {
25782594

25792595
Headers list(isolate, context, headers);
25802596
args.GetReturnValue().Set(stream->SubmitTrailers(*list, list.length()));
2581-
DEBUG_HTTP2STREAM2(stream, "%d trailing headers sent", headers->Length());
2597+
DEBUG_HTTP2STREAM2(stream, "%d trailing headers sent", list.length());
25822598
}
25832599

25842600
// Grab the numeric id of the Http2Stream

0 commit comments

Comments
 (0)