Skip to content

Commit 95bd929

Browse files
authored
refactor: avoid http2 dynamic dispatch in socket handlers (#2839)
1 parent 2a368b2 commit 95bd929

File tree

2 files changed

+137
-104
lines changed

2 files changed

+137
-104
lines changed

lib/core/symbols.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,6 @@ module.exports = {
5959
kHTTP2CopyHeaders: Symbol('http2 copy headers'),
6060
kHTTPConnVersion: Symbol('http connection version'),
6161
kRetryHandlerDefaultRetry: Symbol('retry agent default retry'),
62-
kConstruct: Symbol('constructable')
62+
kConstruct: Symbol('constructable'),
63+
kListeners: Symbol('listeners')
6364
}

lib/dispatcher/client.js

Lines changed: 135 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ const {
7272
kLocalAddress,
7373
kMaxResponseSize,
7474
kHTTPConnVersion,
75+
kListeners,
7576
// HTTP2
7677
kHost,
7778
kHTTP2Session,
@@ -111,6 +112,20 @@ const FastBuffer = Buffer[Symbol.species]
111112

112113
const kClosedResolve = Symbol('kClosedResolve')
113114

115+
function addListener (obj, name, listener) {
116+
const listeners = (obj[kListeners] ??= [])
117+
listeners.push([name, listener])
118+
obj.on(name, listener)
119+
return obj
120+
}
121+
122+
function removeAllListeners (obj) {
123+
for (const [name, listener] of obj[kListeners] ?? []) {
124+
obj.removeListener(name, listener)
125+
}
126+
obj[kListeners] = null
127+
}
128+
114129
/**
115130
* @type {import('../../types/client.js').default}
116131
*/
@@ -276,7 +291,7 @@ class Client extends DispatcherBase {
276291
this[kMaxRequests] = maxRequestsPerClient
277292
this[kClosedResolve] = null
278293
this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1
279-
this[kHTTPConnVersion] = 'h1'
294+
this[kHTTPConnVersion] = null
280295

281296
// HTTP/2
282297
this[kHTTP2Session] = null
@@ -803,11 +818,8 @@ class Parser {
803818

804819
socket[kClient] = null
805820
socket[kError] = null
806-
socket
807-
.removeListener('error', onSocketError)
808-
.removeListener('readable', onSocketReadable)
809-
.removeListener('end', onSocketEnd)
810-
.removeListener('close', onSocketClose)
821+
822+
removeAllListeners(socket)
811823

812824
client[kSocket] = null
813825
client[kHTTP2Session] = null
@@ -1050,33 +1062,6 @@ function onParserTimeout (parser) {
10501062
}
10511063
}
10521064

1053-
function onSocketReadable () {
1054-
const { [kParser]: parser } = this
1055-
if (parser) {
1056-
parser.readMore()
1057-
}
1058-
}
1059-
1060-
function onSocketError (err) {
1061-
const { [kClient]: client, [kParser]: parser } = this
1062-
1063-
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
1064-
1065-
if (client[kHTTPConnVersion] !== 'h2') {
1066-
// On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
1067-
// to the user.
1068-
if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
1069-
// We treat all incoming data so for as a valid response.
1070-
parser.onMessageComplete()
1071-
return
1072-
}
1073-
}
1074-
1075-
this[kError] = err
1076-
1077-
onError(this[kClient], err)
1078-
}
1079-
10801065
function onError (client, err) {
10811066
if (
10821067
client[kRunning] === 0 &&
@@ -1097,32 +1082,8 @@ function onError (client, err) {
10971082
}
10981083
}
10991084

1100-
function onSocketEnd () {
1101-
const { [kParser]: parser, [kClient]: client } = this
1102-
1103-
if (client[kHTTPConnVersion] !== 'h2') {
1104-
if (parser.statusCode && !parser.shouldKeepAlive) {
1105-
// We treat all incoming data so far as a valid response.
1106-
parser.onMessageComplete()
1107-
return
1108-
}
1109-
}
1110-
1111-
util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this)))
1112-
}
1113-
11141085
function onSocketClose () {
1115-
const { [kClient]: client, [kParser]: parser } = this
1116-
1117-
if (client[kHTTPConnVersion] === 'h1' && parser) {
1118-
if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
1119-
// We treat all incoming data so far as a valid response.
1120-
parser.onMessageComplete()
1121-
}
1122-
1123-
this[kParser].destroy()
1124-
this[kParser] = null
1125-
}
1086+
const { [kClient]: client } = this
11261087

11271088
const err = this[kError] || new SocketError('closed', util.getSocketInfo(this))
11281089

@@ -1215,56 +1176,19 @@ async function connect (client) {
12151176

12161177
assert(socket)
12171178

1218-
const isH2 = socket.alpnProtocol === 'h2'
1219-
if (isH2) {
1220-
if (!h2ExperimentalWarned) {
1221-
h2ExperimentalWarned = true
1222-
process.emitWarning('H2 support is experimental, expect them to change at any time.', {
1223-
code: 'UNDICI-H2'
1224-
})
1225-
}
1226-
1227-
const session = http2.connect(client[kUrl], {
1228-
createConnection: () => socket,
1229-
peerMaxConcurrentStreams: client[kHTTP2SessionState].maxConcurrentStreams
1230-
})
1231-
1232-
client[kHTTPConnVersion] = 'h2'
1233-
session[kClient] = client
1234-
session[kSocket] = socket
1235-
session.on('error', onHttp2SessionError)
1236-
session.on('frameError', onHttp2FrameError)
1237-
session.on('end', onHttp2SessionEnd)
1238-
session.on('goaway', onHTTP2GoAway)
1239-
session.on('close', onSocketClose)
1240-
session.unref()
1241-
1242-
client[kHTTP2Session] = session
1243-
socket[kHTTP2Session] = session
1179+
if (socket.alpnProtocol === 'h2') {
1180+
await connectH2(client, socket)
12441181
} else {
1245-
if (!llhttpInstance) {
1246-
llhttpInstance = await llhttpPromise
1247-
llhttpPromise = null
1248-
}
1249-
1250-
socket[kNoRef] = false
1251-
socket[kWriting] = false
1252-
socket[kReset] = false
1253-
socket[kBlocking] = false
1254-
socket[kParser] = new Parser(client, socket, llhttpInstance)
1182+
await connectH1(client, socket)
12551183
}
12561184

1185+
addListener(socket, 'close', onSocketClose)
1186+
12571187
socket[kCounter] = 0
12581188
socket[kMaxRequests] = client[kMaxRequests]
12591189
socket[kClient] = client
12601190
socket[kError] = null
12611191

1262-
socket
1263-
.on('error', onSocketError)
1264-
.on('readable', onSocketReadable)
1265-
.on('end', onSocketEnd)
1266-
.on('close', onSocketClose)
1267-
12681192
client[kSocket] = socket
12691193

12701194
if (channels.connected.hasSubscribers) {
@@ -1475,10 +1399,15 @@ function shouldSendContentLength (method) {
14751399

14761400
function write (client, request) {
14771401
if (client[kHTTPConnVersion] === 'h2') {
1478-
writeH2(client, client[kHTTP2Session], request)
1479-
return
1402+
// TODO (fix): Why does this not return the value
1403+
// from writeH2.
1404+
writeH2(client, request)
1405+
} else {
1406+
return writeH1(client, request)
14801407
}
1408+
}
14811409

1410+
function writeH1 (client, request) {
14821411
const { method, path, host, upgrade, blocking, reset } = request
14831412

14841413
let { body, headers, contentLength } = request
@@ -1656,7 +1585,8 @@ function write (client, request) {
16561585
return true
16571586
}
16581587

1659-
function writeH2 (client, session, request) {
1588+
function writeH2 (client, request) {
1589+
const session = client[kHTTP2Session]
16601590
const { body, method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request
16611591

16621592
let headers
@@ -2341,4 +2271,106 @@ function errorRequest (client, request, err) {
23412271
}
23422272
}
23432273

2274+
async function connectH1 (client, socket) {
2275+
client[kHTTPConnVersion] = 'h1'
2276+
2277+
if (!llhttpInstance) {
2278+
llhttpInstance = await llhttpPromise
2279+
llhttpPromise = null
2280+
}
2281+
2282+
socket[kNoRef] = false
2283+
socket[kWriting] = false
2284+
socket[kReset] = false
2285+
socket[kBlocking] = false
2286+
socket[kParser] = new Parser(client, socket, llhttpInstance)
2287+
2288+
addListener(socket, 'error', function (err) {
2289+
const { [kParser]: parser } = this
2290+
2291+
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
2292+
2293+
// On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
2294+
// to the user.
2295+
if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
2296+
// We treat all incoming data so for as a valid response.
2297+
parser.onMessageComplete()
2298+
return
2299+
}
2300+
2301+
this[kError] = err
2302+
2303+
onError(this[kClient], err)
2304+
})
2305+
addListener(socket, 'readable', function () {
2306+
const { [kParser]: parser } = this
2307+
if (parser) {
2308+
parser.readMore()
2309+
}
2310+
})
2311+
addListener(socket, 'end', function () {
2312+
const { [kParser]: parser } = this
2313+
2314+
if (parser.statusCode && !parser.shouldKeepAlive) {
2315+
// We treat all incoming data so far as a valid response.
2316+
parser.onMessageComplete()
2317+
return
2318+
}
2319+
2320+
util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this)))
2321+
})
2322+
addListener(socket, 'close', function () {
2323+
const { [kParser]: parser } = this
2324+
2325+
if (parser) {
2326+
if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
2327+
// We treat all incoming data so far as a valid response.
2328+
parser.onMessageComplete()
2329+
}
2330+
2331+
this[kParser].destroy()
2332+
this[kParser] = null
2333+
}
2334+
})
2335+
}
2336+
2337+
async function connectH2 (client, socket) {
2338+
client[kHTTPConnVersion] = 'h2'
2339+
2340+
if (!h2ExperimentalWarned) {
2341+
h2ExperimentalWarned = true
2342+
process.emitWarning('H2 support is experimental, expect them to change at any time.', {
2343+
code: 'UNDICI-H2'
2344+
})
2345+
}
2346+
2347+
const session = http2.connect(client[kUrl], {
2348+
createConnection: () => socket,
2349+
peerMaxConcurrentStreams: client[kHTTP2SessionState].maxConcurrentStreams
2350+
})
2351+
2352+
session[kClient] = client
2353+
session[kSocket] = socket
2354+
session.on('error', onHttp2SessionError)
2355+
session.on('frameError', onHttp2FrameError)
2356+
session.on('end', onHttp2SessionEnd)
2357+
session.on('goaway', onHTTP2GoAway)
2358+
session.on('close', onSocketClose)
2359+
session.unref()
2360+
2361+
client[kHTTP2Session] = session
2362+
socket[kHTTP2Session] = session
2363+
2364+
addListener(socket, 'error', function (err) {
2365+
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
2366+
2367+
this[kError] = err
2368+
2369+
onError(this[kClient], err)
2370+
})
2371+
addListener(socket, 'end', function () {
2372+
util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this)))
2373+
})
2374+
}
2375+
23442376
module.exports = Client

0 commit comments

Comments
 (0)