Skip to content

Commit e3c6629

Browse files
mcollinaaduh95
authored andcommitted
http2: emit session close before stream close
Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #63414Fixes: #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 be91f0a commit e3c6629

4 files changed

Lines changed: 110 additions & 26 deletions

File tree

‎doc/api/http2.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1113,10 +1113,12 @@ creates and returns an `Http2Stream` instance that can be used to send an
11131113
HTTP/2 request to the connected server.
11141114

11151115
When a `ClientHttp2Session` is first created, the socket may not yet be
1116-
connected. if`clienthttp2session.request()` is called during this time, the
1116+
connected. If`clienthttp2session.request()` is called during this time, the
11171117
actual request will be deferred until the socket is ready to go.
1118-
If the `session` is closed before the actual request be executed, an
1119-
`ERR_HTTP2_GOAWAY_SESSION` is thrown.
1118+
1119+
If the session becomes unavailable before the request can be created, the
1120+
returned stream will emit `ERR_HTTP2_GOAWAY_SESSION` or
1121+
`ERR_HTTP2_INVALID_SESSION` asynchronously.
11201122

11211123
This method is only available if `http2session.type` is equal to
11221124
`http2.constants.NGHTTP2_SESSION_CLIENT`.

‎lib/internal/http2/core.js‎

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,10 @@ function requestOnConnect(headersList, options) {
838838
}
839839
}
840840

841+
functionrequestOnError(error){
842+
this.destroy(error);
843+
}
844+
841845
// Validates that priority options are correct, specifically:
842846
// 1. options.weight must be a number
843847
// 2. options.parent must be a positive number
@@ -1153,7 +1157,7 @@ function setupHandle(socket, type, options) {
11531157
process.nextTick(emit,this,'connect',this,socket);
11541158
}
11551159

1156-
// Emits a close event followed by an error event if err is truthy. Used
1160+
// Emits an error event followed by a close event if err is truthy. Used
11571161
// by Http2Session.prototype.destroy()
11581162
functionemitClose(self,error){
11591163
if(error)
@@ -1224,17 +1228,16 @@ function closeSession(session, code, error) {
12241228
session.setTimeout(0);
12251229
session.removeAllListeners('timeout');
12261230

1231+
constsocket=session[kSocket];
1232+
consthandle=session[kHandle];
1233+
12271234
// Destroy any pending and open streams
12281235
if(state.pendingStreams.size>0||state.streams.size>0){
12291236
constcancel=newERR_HTTP2_STREAM_CANCEL(error);
12301237
state.pendingStreams.forEach((stream)=>stream.destroy(cancel));
12311238
state.streams.forEach((stream)=>stream.destroy(error));
12321239
}
12331240

1234-
// Disassociate from the socket and server.
1235-
constsocket=session[kSocket];
1236-
consthandle=session[kHandle];
1237-
12381241
// Destroy the handle if it exists at this point.
12391242
if(handle!==undefined){
12401243
handle.ondone=finishSessionClose.bind(null,session,error);
@@ -1809,11 +1812,15 @@ class ClientHttp2Session extends Http2Session {
18091812
request(headersParam,options){
18101813
debugSessionObj(this,'initiating request');
18111814

1812-
if(this.destroyed)
1813-
thrownewERR_HTTP2_INVALID_SESSION();
1814-
1815-
if(this.closed)
1816-
thrownewERR_HTTP2_GOAWAY_SESSION();
1815+
// Keep argument validation synchronous, but defer session-state failures
1816+
// to the returned stream so request retries from stream callbacks do not
1817+
// throw before session lifecycle handlers run.
1818+
letrequestError;
1819+
if(this.destroyed){
1820+
requestError=newERR_HTTP2_INVALID_SESSION();
1821+
}elseif(this.closed){
1822+
requestError=newERR_HTTP2_GOAWAY_SESSION();
1823+
}
18171824

18181825
this[kUpdateTimer]();
18191826

@@ -1899,19 +1906,24 @@ class ClientHttp2Session extends Http2Session {
18991906
}
19001907
}
19011908

1902-
constonConnect=reqAsync.bind(requestOnConnect.bind(stream,headersList,options));
1903-
if(this.connecting){
1904-
if(this[kPendingRequestCalls]!==null){
1905-
this[kPendingRequestCalls].push(onConnect);
1909+
if(requestError){
1910+
process.nextTick(reqAsync.bind(requestOnError.bind(stream,requestError)));
1911+
}else{
1912+
constonConnect=reqAsync.bind(
1913+
requestOnConnect.bind(stream,headersList,options));
1914+
if(this.connecting){
1915+
if(this[kPendingRequestCalls]!==null){
1916+
this[kPendingRequestCalls].push(onConnect);
1917+
}else{
1918+
this[kPendingRequestCalls]=[onConnect];
1919+
this.once('connect',()=>{
1920+
this[kPendingRequestCalls].forEach((f)=>f());
1921+
this[kPendingRequestCalls]=null;
1922+
});
1923+
}
19061924
}else{
1907-
this[kPendingRequestCalls]=[onConnect];
1908-
this.once('connect',()=>{
1909-
this[kPendingRequestCalls].forEach((f)=>f());
1910-
this[kPendingRequestCalls]=null;
1911-
});
1925+
onConnect();
19121926
}
1913-
}else{
1914-
onConnect();
19151927
}
19161928

19171929
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+
constpendingReq=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+
constpostCloseReq=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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use strict';
2+
3+
constcommon=require('../common');
4+
if(!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
constassert=require('assert');
8+
consthttp2=require('http2');
9+
10+
constserver=http2.createServer();
11+
letserverSocket;
12+
13+
server.on('connection',common.mustCall((socket)=>{
14+
serverSocket=socket;
15+
socket.on('error',()=>{});
16+
}));
17+
18+
server.on('sessionError',()=>{});
19+
server.on('stream',common.mustCall((stream,headers)=>{
20+
if(headers[':path']==='/close'){
21+
stream.respond({':status': 200});
22+
stream.write('partial',common.mustCall(()=>{
23+
setImmediate(()=>serverSocket.destroy());
24+
}));
25+
return;
26+
}
27+
28+
stream.respond({':status': 200});
29+
stream.end('ok');
30+
}));
31+
32+
server.listen(0,common.mustCall(()=>{
33+
constsession=http2.connect(`http://localhost:${server.address().port}`);
34+
letcachedSession=session;
35+
36+
session.on('error',()=>{});
37+
session.on('close',common.mustCall(()=>{
38+
cachedSession=undefined;
39+
server.close();
40+
}));
41+
42+
constreq=session.request({':path': '/close'});
43+
req.on('response',common.mustCall());
44+
req.on('error',()=>{});
45+
req.on('close',common.mustCall(()=>{
46+
// This must not throw synchronously even though the session is no longer
47+
// usable. Depending on teardown timing, the returned stream may report a
48+
// closed session before the destroy state is fully observable here.
49+
constreq2=session.request({':path': '/again'});
50+
51+
req2.on('error',common.mustCall((err)=>{
52+
assert.ok(
53+
err.code==='ERR_HTTP2_INVALID_SESSION'||
54+
err.code==='ERR_HTTP2_GOAWAY_SESSION');
55+
assert.strictEqual(cachedSession,undefined);
56+
}));
57+
}));
58+
req.resume();
59+
}));

0 commit comments

Comments
 (0)