Skip to content

Commit 2ba99a3

Browse files
authored
fix: wrap kConnector call in try/catch to prevent client hang (#4834)
1 parent a7398c0 commit 2ba99a3

2 files changed

Lines changed: 143 additions & 53 deletions

File tree

lib/dispatcher/client.js

Lines changed: 58 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -452,65 +452,70 @@ function connect (client) {
452452
})
453453
}
454454

455-
client[kConnector]({
456-
host,
457-
hostname,
458-
protocol,
459-
port,
460-
servername: client[kServerName],
461-
localAddress: client[kLocalAddress]
462-
}, (err, socket) => {
463-
if (err) {
464-
handleConnectError(client, err, { host, hostname, protocol, port })
465-
client[kResume]()
466-
return
467-
}
455+
try {
456+
client[kConnector]({
457+
host,
458+
hostname,
459+
protocol,
460+
port,
461+
servername: client[kServerName],
462+
localAddress: client[kLocalAddress]
463+
}, (err, socket) => {
464+
if (err) {
465+
handleConnectError(client, err, { host, hostname, protocol, port })
466+
client[kResume]()
467+
return
468+
}
468469

469-
if (client.destroyed) {
470-
util.destroy(socket.on('error', noop), new ClientDestroyedError())
471-
client[kResume]()
472-
return
473-
}
470+
if (client.destroyed) {
471+
util.destroy(socket.on('error', noop), new ClientDestroyedError())
472+
client[kResume]()
473+
return
474+
}
474475

475-
assert(socket)
476+
assert(socket)
476477

477-
try {
478-
client[kHTTPContext] = socket.alpnProtocol === 'h2'
479-
? connectH2(client, socket)
480-
: connectH1(client, socket)
481-
} catch (err) {
482-
socket.destroy().on('error', noop)
483-
handleConnectError(client, err, { host, hostname, protocol, port })
484-
client[kResume]()
485-
return
486-
}
478+
try {
479+
client[kHTTPContext] = socket.alpnProtocol === 'h2'
480+
? connectH2(client, socket)
481+
: connectH1(client, socket)
482+
} catch (err) {
483+
socket.destroy().on('error', noop)
484+
handleConnectError(client, err, { host, hostname, protocol, port })
485+
client[kResume]()
486+
return
487+
}
487488

488-
client[kConnecting] = false
489-
490-
socket[kCounter] = 0
491-
socket[kMaxRequests] = client[kMaxRequests]
492-
socket[kClient] = client
493-
socket[kError] = null
494-
495-
if (channels.connected.hasSubscribers) {
496-
channels.connected.publish({
497-
connectParams: {
498-
host,
499-
hostname,
500-
protocol,
501-
port,
502-
version: client[kHTTPContext]?.version,
503-
servername: client[kServerName],
504-
localAddress: client[kLocalAddress]
505-
},
506-
connector: client[kConnector],
507-
socket
508-
})
509-
}
489+
client[kConnecting] = false
490+
491+
socket[kCounter] = 0
492+
socket[kMaxRequests] = client[kMaxRequests]
493+
socket[kClient] = client
494+
socket[kError] = null
495+
496+
if (channels.connected.hasSubscribers) {
497+
channels.connected.publish({
498+
connectParams: {
499+
host,
500+
hostname,
501+
protocol,
502+
port,
503+
version: client[kHTTPContext]?.version,
504+
servername: client[kServerName],
505+
localAddress: client[kLocalAddress]
506+
},
507+
connector: client[kConnector],
508+
socket
509+
})
510+
}
510511

511-
client.emit('connect', client[kUrl], [client])
512+
client.emit('connect', client[kUrl], [client])
513+
client[kResume]()
514+
})
515+
} catch (err) {
516+
handleConnectError(client, err, { host, hostname, protocol, port })
512517
client[kResume]()
513-
})
518+
}
514519
}
515520

516521
function handleConnectError (client, err, { host, hostname, protocol, port }) {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
'use strict'
2+
3+
const { test, after } = require('node:test')
4+
const { tspl } = require('@matteo.collina/tspl')
5+
const { Client } = require('../..')
6+
const { createServer } = require('node:http')
7+
8+
const { closeServerAsPromise } = require('../utils/node-http')
9+
10+
test('client does not hang when connector throws synchronously', async (t) => {
11+
const p = tspl(t, { plan: 4 })
12+
13+
// We need a server just so the URL resolves, but we'll never actually
14+
// connect because our custom connector throws before that.
15+
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
16+
res.end()
17+
})
18+
after(closeServerAsPromise(server))
19+
20+
server.listen(0, () => {
21+
const client = new Client(`http://localhost:${server.address().port}`, {
22+
connect: () => {
23+
throw new Error('connector boom')
24+
}
25+
})
26+
after(() => client.destroy())
27+
28+
// First request should get the error from the throwing connector
29+
client.request({ path: '/', method: 'GET' }, (err) => {
30+
p.ok(err instanceof Error)
31+
p.strictEqual(err.message, 'connector boom')
32+
33+
// Second request should also get an error and not hang
34+
client.request({ path: '/', method: 'GET' }, (err) => {
35+
p.ok(err instanceof Error)
36+
p.strictEqual(err.message, 'connector boom')
37+
})
38+
})
39+
})
40+
41+
await p.completed
42+
})
43+
44+
test('client recovers after connector stops throwing', async (t) => {
45+
const p = tspl(t, { plan: 4 })
46+
47+
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
48+
res.writeHead(200)
49+
res.end('ok')
50+
})
51+
after(closeServerAsPromise(server))
52+
53+
server.listen(0, () => {
54+
let shouldThrow = true
55+
const client = new Client(`http://localhost:${server.address().port}`, {
56+
connect: (opts, cb) => {
57+
if (shouldThrow) {
58+
throw new Error('connector boom')
59+
}
60+
// Fall back to default connector behavior via tls/net
61+
const net = require('node:net')
62+
const socket = net.connect(opts.port, opts.hostname)
63+
socket.on('connect', () => cb(null, socket))
64+
socket.on('error', (err) => cb(err, null))
65+
}
66+
})
67+
after(() => client.destroy())
68+
69+
// First request fails because connector throws
70+
client.request({ path: '/', method: 'GET' }, (err) => {
71+
p.ok(err instanceof Error)
72+
p.strictEqual(err.message, 'connector boom')
73+
74+
// Now stop throwing so the next request can succeed
75+
shouldThrow = false
76+
77+
client.request({ path: '/', method: 'GET' }, (err, data) => {
78+
p.ifError(err)
79+
p.strictEqual(data.statusCode, 200)
80+
})
81+
})
82+
})
83+
84+
await p.completed
85+
})

0 commit comments

Comments
 (0)