http2: emit session close before stream close · nodejs/node@39f61fb · GitHub
Skip to content

Commit 39f61fb

Browse files
mcollinasxa
authored andcommitted
http2: emit session close before stream close
Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #63414 Fixes: #63412 Reviewed-By: Tim Perry <pimterry@gmail.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
1 parent 5a95a2b commit 39f61fb

4 files changed

Lines changed: 110 additions & 26 deletions

File tree

doc/api/http2.md

Lines changed: 5 additions & 3 deletions

lib/internal/http2/core.js

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,10 @@ function requestOnConnect(headersList, options) {
845845
}
846846
}
847847

848+
function requestOnError(error) {
849+
this.destroy(error);
850+
}
851+
848852
// Validates that priority options are correct, specifically:
849853
// 1. options.weight must be a number
850854
// 2. options.parent must be a positive number
@@ -1160,7 +1164,7 @@ function setupHandle(socket, type, options) {
11601164
process.nextTick(emit, this, 'connect', this, socket);
11611165
}
11621166

1163-
// Emits a close event followed by an error event if err is truthy. Used
1167+
// Emits an error event followed by a close event if err is truthy. Used
11641168
// by Http2Session.prototype.destroy()
11651169
function emitClose(self, error) {
11661170
if (error)
@@ -1231,17 +1235,16 @@ function closeSession(session, code, error) {
12311235
session.setTimeout(0);
12321236
session.removeAllListeners('timeout');
12331237

1238+
const socket = session[kSocket];
1239+
const handle = session[kHandle];
1240+
12341241
// Destroy any pending and open streams
12351242
if (state.pendingStreams.size > 0 || state.streams.size > 0) {
12361243
const cancel = new ERR_HTTP2_STREAM_CANCEL(error);
12371244
state.pendingStreams.forEach((stream) => stream.destroy(cancel));
12381245
state.streams.forEach((stream) => stream.destroy(error));
12391246
}
12401247

1241-
// Disassociate from the socket and server.
1242-
const socket = session[kSocket];
1243-
const handle = session[kHandle];
1244-
12451248
// Destroy the handle if it exists at this point.
12461249
if (handle !== undefined) {
12471250
handle.ondone = finishSessionClose.bind(null, session, error);
@@ -1816,11 +1819,15 @@ class ClientHttp2Session extends Http2Session {
18161819
request(headersParam, options) {
18171820
debugSessionObj(this, 'initiating request');
18181821

1819-
if (this.destroyed)
1820-
throw new ERR_HTTP2_INVALID_SESSION();
1821-
1822-
if (this.closed)
1823-
throw new ERR_HTTP2_GOAWAY_SESSION();
1822+
// Keep argument validation synchronous, but defer session-state failures
1823+
// to the returned stream so request retries from stream callbacks do not
1824+
// throw before session lifecycle handlers run.
1825+
let requestError;
1826+
if (this.destroyed) {
1827+
requestError = new ERR_HTTP2_INVALID_SESSION();
1828+
} else if (this.closed) {
1829+
requestError = new ERR_HTTP2_GOAWAY_SESSION();
1830+
}
18241831

18251832
this[kUpdateTimer]();
18261833

@@ -1906,19 +1913,24 @@ class ClientHttp2Session extends Http2Session {
19061913
}
19071914
}
19081915

1909-
const onConnect = reqAsync.bind(requestOnConnect.bind(stream, headersList, options));
1910-
if (this.connecting) {
1911-
if (this[kPendingRequestCalls] !== null) {
1912-
this[kPendingRequestCalls].push(onConnect);
1916+
if (requestError) {
1917+
process.nextTick(reqAsync.bind(requestOnError.bind(stream, requestError)));
1918+
} else {
1919+
const onConnect = reqAsync.bind(
1920+
requestOnConnect.bind(stream, headersList, options));
1921+
if (this.connecting) {
1922+
if (this[kPendingRequestCalls] !== null) {
1923+
this[kPendingRequestCalls].push(onConnect);
1924+
} else {
1925+
this[kPendingRequestCalls] = [onConnect];
1926+
this.once('connect', () => {
1927+
this[kPendingRequestCalls].forEach((f) => f());
1928+
this[kPendingRequestCalls] = null;
1929+
});
1930+
}
19131931
} else {
1914-
this[kPendingRequestCalls] = [onConnect];
1915-
this.once('connect', () => {
1916-
this[kPendingRequestCalls].forEach((f) => f());
1917-
this[kPendingRequestCalls] = null;
1918-
});
1932+
onConnect();
19191933
}
1920-
} else {
1921-
onConnect();
19221934
}
19231935

19241936
if (onClientStreamCreatedChannel.hasSubscribers) {

test/parallel/test-http2-client-destroy.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,19 @@ const { listenerCount } = require('events');
8181
assert.throws(() => client.ping(), sessionError);
8282
assert.throws(() => client.settings({}), sessionError);
8383
assert.throws(() => client.goaway(), sessionError);
84-
assert.throws(() => client.request(), sessionError);
84+
85+
const pendingReq = client.request();
86+
pendingReq.on('response', common.mustNotCall());
87+
pendingReq.on('error', common.expectsError(sessionError));
88+
pendingReq.on('close', common.mustCall());
89+
90+
client.on('close', common.mustCall(() => {
91+
const postCloseReq = client.request();
92+
postCloseReq.on('response', common.mustNotCall());
93+
postCloseReq.on('error', common.expectsError(sessionError));
94+
postCloseReq.on('close', common.mustCall());
95+
}));
96+
8597
client.close(); // Should be a non-op at this point
8698

8799
// Wait for setImmediate call from destroy() to complete
@@ -92,7 +104,6 @@ const { listenerCount } = require('events');
92104
assert.throws(() => client.ping(), sessionError);
93105
assert.throws(() => client.settings({}), sessionError);
94106
assert.throws(() => client.goaway(), sessionError);
95-
assert.throws(() => client.request(), sessionError);
96107
client.close(); // Should be a non-op at this point
97108
}));
98109

Lines changed: 59 additions & 0 deletions

0 commit comments

Comments
 (0)