Skip to content

Commit 41dca9e

Browse files
apapirovskiBethGriggs
authored andcommitted
http2: fix responses to long payload reqs
When a request with a long payload is received, http2 does not allow a response that does not process all the incoming payload. Add a conditional Http2Stream.close call that runs only if the user hasn't attempted to read the stream. Backport-PR-URL: #22850 PR-URL: #20084 Fixes: #20060 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent 1a6a054 commit 41dca9e

10 files changed

+281
-105
lines changed

lib/internal/http2/core.js

+99-82
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ const STREAM_FLAGS_CLOSED = 0x2;
154154
const STREAM_FLAGS_HEADERS_SENT = 0x4;
155155
const STREAM_FLAGS_HEAD_REQUEST = 0x8;
156156
const STREAM_FLAGS_ABORTED = 0x10;
157+
const STREAM_FLAGS_HAS_TRAILERS = 0x20;
157158

158159
const SESSION_FLAGS_PENDING = 0x0;
159160
const SESSION_FLAGS_READY = 0x1;
@@ -278,27 +279,14 @@ function onStreamClose(code) {
278279
if (stream.destroyed)
279280
return;
280281

281-
const state = stream[kState];
282-
283282
debug(`Http2Stream ${stream[kID]} [Http2Session ` +
284283
`${sessionName(stream[kSession][kType])}]: closed with code ${code}`);
285284

286-
if (!stream.closed) {
287-
// Unenroll from timeouts
288-
unenroll(stream);
289-
stream.removeAllListeners('timeout');
290-
291-
// Set the state flags
292-
state.flags |= STREAM_FLAGS_CLOSED;
293-
state.rstCode = code;
294-
295-
// Close the writable side of the stream
296-
abort(stream);
297-
stream.end();
298-
}
285+
if (!stream.closed)
286+
closeStream(stream, code, false);
299287

300-
if (state.fd !== undefined)
301-
tryClose(state.fd);
288+
if (stream[kState].fd !== undefined)
289+
tryClose(stream[kState].fd);
302290

303291
// Defer destroy we actually emit end.
304292
if (stream._readableState.endEmitted || code !== NGHTTP2_NO_ERROR) {
@@ -454,7 +442,7 @@ function requestOnConnect(headers, options) {
454442

455443
// At this point, the stream should have already been destroyed during
456444
// the session.destroy() method. Do nothing else.
457-
if (session.destroyed)
445+
if (session === undefined || session.destroyed)
458446
return;
459447

460448
// If the session was closed while waiting for the connect, destroy
@@ -1369,6 +1357,9 @@ class ClientHttp2Session extends Http2Session {
13691357
if (options.endStream)
13701358
stream.end();
13711359

1360+
if (options.waitForTrailers)
1361+
stream[kState].flags |= STREAM_FLAGS_HAS_TRAILERS;
1362+
13721363
const onConnect = requestOnConnect.bind(stream, headersList, options);
13731364
if (this.connecting) {
13741365
this.on('connect', onConnect);
@@ -1425,32 +1416,70 @@ function afterDoStreamWrite(status, handle, req) {
14251416
}
14261417

14271418
function streamOnResume() {
1428-
if (!this.destroyed && !this.pending)
1419+
if (!this.destroyed && !this.pending) {
1420+
if (!this[kState].didRead)
1421+
this[kState].didRead = true;
14291422
this[kHandle].readStart();
1423+
}
14301424
}
14311425

14321426
function streamOnPause() {
14331427
if (!this.destroyed && !this.pending)
14341428
this[kHandle].readStop();
14351429
}
14361430

1437-
// If the writable side of the Http2Stream is still open, emit the
1438-
// 'aborted' event and set the aborted flag.
1439-
function abort(stream) {
1440-
if (!stream.aborted &&
1441-
!(stream._writableState.ended || stream._writableState.ending)) {
1442-
stream[kState].flags |= STREAM_FLAGS_ABORTED;
1443-
stream.emit('aborted');
1444-
}
1445-
}
1446-
14471431
function afterShutdown() {
14481432
this.callback();
14491433
const stream = this.handle[kOwner];
14501434
if (stream)
14511435
stream[kMaybeDestroy]();
14521436
}
14531437

1438+
function closeStream(stream, code, shouldSubmitRstStream = true) {
1439+
const state = stream[kState];
1440+
state.flags |= STREAM_FLAGS_CLOSED;
1441+
state.rstCode = code;
1442+
1443+
// Clear timeout and remove timeout listeners
1444+
stream.setTimeout(0);
1445+
stream.removeAllListeners('timeout');
1446+
1447+
const { ending, finished } = stream._writableState;
1448+
1449+
if (!ending) {
1450+
// If the writable side of the Http2Stream is still open, emit the
1451+
// 'aborted' event and set the aborted flag.
1452+
if (!stream.aborted) {
1453+
state.flags |= STREAM_FLAGS_ABORTED;
1454+
stream.emit('aborted');
1455+
}
1456+
1457+
// Close the writable side.
1458+
stream.end();
1459+
}
1460+
1461+
if (shouldSubmitRstStream) {
1462+
const finishFn = finishCloseStream.bind(stream, code);
1463+
if (!ending || finished || code !== NGHTTP2_NO_ERROR)
1464+
finishFn();
1465+
else
1466+
stream.once('finish', finishFn);
1467+
}
1468+
}
1469+
1470+
function finishCloseStream(code) {
1471+
const rstStreamFn = submitRstStream.bind(this, code);
1472+
// If the handle has not yet been assigned, queue up the request to
1473+
// ensure that the RST_STREAM frame is sent after the stream ID has
1474+
// been determined.
1475+
if (this.pending) {
1476+
this.push(null);
1477+
this.once('ready', rstStreamFn);
1478+
return;
1479+
}
1480+
rstStreamFn();
1481+
}
1482+
14541483
// An Http2Stream is a Duplex stream that is backed by a
14551484
// node::http2::Http2Stream handle implementing StreamBase.
14561485
class Http2Stream extends Duplex {
@@ -1468,6 +1497,7 @@ class Http2Stream extends Duplex {
14681497
session[kState].pendingStreams.add(this);
14691498

14701499
this[kState] = {
1500+
didRead: false,
14711501
flags: STREAM_FLAGS_PENDING,
14721502
rstCode: NGHTTP2_NO_ERROR,
14731503
writeQueueSize: 0,
@@ -1749,6 +1779,8 @@ class Http2Stream extends Duplex {
17491779
throw headersList;
17501780
this[kSentTrailers] = headers;
17511781

1782+
this[kState].flags &= ~STREAM_FLAGS_HAS_TRAILERS;
1783+
17521784
const ret = this[kHandle].trailers(headersList);
17531785
if (ret < 0)
17541786
this.destroy(new NghttpError(ret));
@@ -1779,38 +1811,13 @@ class Http2Stream extends Duplex {
17791811
if (callback !== undefined && typeof callback !== 'function')
17801812
throw new errors.TypeError('ERR_INVALID_CALLBACK');
17811813

1782-
// Unenroll the timeout.
1783-
unenroll(this);
1784-
this.removeAllListeners('timeout');
1785-
1786-
// Close the writable
1787-
abort(this);
1788-
this.end();
1789-
17901814
if (this.closed)
17911815
return;
17921816

1793-
const state = this[kState];
1794-
state.flags |= STREAM_FLAGS_CLOSED;
1795-
state.rstCode = code;
1796-
1797-
if (callback !== undefined) {
1817+
if (callback !== undefined)
17981818
this.once('close', callback);
1799-
}
18001819

1801-
if (this[kHandle] === undefined)
1802-
return;
1803-
1804-
const rstStreamFn = submitRstStream.bind(this, code);
1805-
// If the handle has not yet been assigned, queue up the request to
1806-
// ensure that the RST_STREAM frame is sent after the stream ID has
1807-
// been determined.
1808-
if (this.pending) {
1809-
this.push(null);
1810-
this.once('ready', rstStreamFn);
1811-
return;
1812-
}
1813-
rstStreamFn();
1820+
closeStream(this, code);
18141821
}
18151822

18161823
// Called by this.destroy().
@@ -1825,24 +1832,19 @@ class Http2Stream extends Duplex {
18251832
debug(`Http2Stream ${this[kID] || '<pending>'} [Http2Session ` +
18261833
`${sessionName(session[kType])}]: destroying stream`);
18271834
const state = this[kState];
1828-
const code = state.rstCode =
1829-
err != null ?
1830-
NGHTTP2_INTERNAL_ERROR :
1831-
state.rstCode || NGHTTP2_NO_ERROR;
1832-
if (handle !== undefined) {
1833-
// If the handle exists, we need to close, then destroy the handle
1834-
this.close(code);
1835-
if (!this._readableState.ended && !this._readableState.ending)
1836-
this.push(null);
1835+
const code = err != null ?
1836+
NGHTTP2_INTERNAL_ERROR : (state.rstCode || NGHTTP2_NO_ERROR);
1837+
1838+
const hasHandle = handle !== undefined;
1839+
1840+
if (!this.closed)
1841+
closeStream(this, code, hasHandle);
1842+
this.push(null);
1843+
1844+
if (hasHandle) {
18371845
handle.destroy();
18381846
session[kState].streams.delete(id);
18391847
} else {
1840-
unenroll(this);
1841-
this.removeAllListeners('timeout');
1842-
state.flags |= STREAM_FLAGS_CLOSED;
1843-
abort(this);
1844-
this.end();
1845-
this.push(null);
18461848
session[kState].pendingStreams.delete(this);
18471849
}
18481850

@@ -1878,13 +1880,23 @@ class Http2Stream extends Duplex {
18781880
}
18791881

18801882
// TODO(mcollina): remove usage of _*State properties
1881-
if (this._readableState.ended &&
1882-
this._writableState.ended &&
1883-
this._writableState.pendingcb === 0 &&
1884-
this.closed) {
1885-
this.destroy();
1886-
// This should return, but eslint complains.
1887-
// return
1883+
if (this._writableState.ended && this._writableState.pendingcb === 0) {
1884+
if (this._readableState.ended && this.closed) {
1885+
this.destroy();
1886+
return;
1887+
}
1888+
1889+
// We've submitted a response from our server session, have not attempted
1890+
// to process any incoming data, and have no trailers. This means we can
1891+
// attempt to gracefully close the session.
1892+
const state = this[kState];
1893+
if (this.headersSent &&
1894+
this[kSession][kType] === NGHTTP2_SESSION_SERVER &&
1895+
!(state.flags & STREAM_FLAGS_HAS_TRAILERS) &&
1896+
!state.didRead &&
1897+
!this._readableState.resumeScheduled) {
1898+
this.close();
1899+
}
18881900
}
18891901
}
18901902
}
@@ -2045,7 +2057,6 @@ function afterOpen(session, options, headers, streamOptions, err, fd) {
20452057
}
20462058
if (this.destroyed || this.closed) {
20472059
tryClose(fd);
2048-
abort(this);
20492060
return;
20502061
}
20512062
state.fd = fd;
@@ -2181,8 +2192,10 @@ class ServerHttp2Stream extends Http2Stream {
21812192
if (options.endStream)
21822193
streamOptions |= STREAM_OPTION_EMPTY_PAYLOAD;
21832194

2184-
if (options.waitForTrailers)
2195+
if (options.waitForTrailers) {
21852196
streamOptions |= STREAM_OPTION_GET_TRAILERS;
2197+
state.flags |= STREAM_FLAGS_HAS_TRAILERS;
2198+
}
21862199

21872200
headers = processHeaders(headers);
21882201
const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
@@ -2248,8 +2261,10 @@ class ServerHttp2Stream extends Http2Stream {
22482261
}
22492262

22502263
let streamOptions = 0;
2251-
if (options.waitForTrailers)
2264+
if (options.waitForTrailers) {
22522265
streamOptions |= STREAM_OPTION_GET_TRAILERS;
2266+
this[kState].flags |= STREAM_FLAGS_HAS_TRAILERS;
2267+
}
22532268

22542269
if (typeof fd !== 'number')
22552270
throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
@@ -2315,8 +2330,10 @@ class ServerHttp2Stream extends Http2Stream {
23152330
}
23162331

23172332
let streamOptions = 0;
2318-
if (options.waitForTrailers)
2333+
if (options.waitForTrailers) {
23192334
streamOptions |= STREAM_OPTION_GET_TRAILERS;
2335+
this[kState].flags |= STREAM_FLAGS_HAS_TRAILERS;
2336+
}
23202337

23212338
const session = this[kSession];
23222339
debug(`Http2Stream ${this[kID]} [Http2Session ` +

0 commit comments

Comments
 (0)