Skip to content

Commit fa5bcb4

Browse files
authored
fix(#2364): concurrent aborts (#3005)
* fix: concurrent aborts * refactor: wording
1 parent 7485cd9 commit fa5bcb4

File tree

2 files changed

+97
-1
lines changed

2 files changed

+97
-1
lines changed

lib/dispatcher/client-h2.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,18 @@ function writeH2 (client, request) {
391391
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
392392
request.onResponseStarted()
393393

394+
// Due to the stream nature, it is possible we face a race condition
395+
// where the stream has been assigned, but the request has been aborted
396+
// the request remains in-flight and headers hasn't been received yet
397+
// for those scenarios, best effort is to destroy the stream immediately
398+
// as there's no value to keep it open.
399+
if (request.aborted || request.completed) {
400+
const err = new RequestAbortedError()
401+
errorRequest(client, request, err)
402+
util.destroy(stream, err)
403+
return
404+
}
405+
394406
if (request.onHeaders(Number(statusCode), realHeaders, stream.resume.bind(stream), '') === false) {
395407
stream.pause()
396408
}
@@ -426,7 +438,6 @@ function writeH2 (client, request) {
426438

427439
stream.once('close', () => {
428440
session[kOpenStreams] -= 1
429-
// TODO(HTTP/2): unref only if current streams count is 0
430441
if (session[kOpenStreams] === 0) {
431442
session.unref()
432443
}

test/http2.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,3 +1294,88 @@ test('Should throw informational error on half-closed streams (remote)', async t
12941294
t.strictEqual(err.code, 'UND_ERR_INFO')
12951295
})
12961296
})
1297+
1298+
test('#2364 - Concurrent aborts', async t => {
1299+
const server = createSecureServer(pem)
1300+
1301+
server.on('stream', (stream, headers, _flags, rawHeaders) => {
1302+
t.strictEqual(headers['x-my-header'], 'foo')
1303+
t.strictEqual(headers[':method'], 'GET')
1304+
setTimeout(() => {
1305+
stream.respond({
1306+
'content-type': 'text/plain; charset=utf-8',
1307+
'x-custom-h2': 'hello',
1308+
':status': 200
1309+
})
1310+
stream.end('hello h2!')
1311+
}, 100)
1312+
})
1313+
1314+
server.listen(0)
1315+
await once(server, 'listening')
1316+
1317+
const client = new Client(`https://localhost:${server.address().port}`, {
1318+
connect: {
1319+
rejectUnauthorized: false
1320+
},
1321+
allowH2: true
1322+
})
1323+
1324+
t = tspl(t, { plan: 18 })
1325+
after(() => server.close())
1326+
after(() => client.close())
1327+
const controller = new AbortController()
1328+
1329+
client.request({
1330+
path: '/',
1331+
method: 'GET',
1332+
headers: {
1333+
'x-my-header': 'foo'
1334+
}
1335+
}, (err, response) => {
1336+
t.ifError(err)
1337+
t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8')
1338+
t.strictEqual(response.headers['x-custom-h2'], 'hello')
1339+
t.strictEqual(response.statusCode, 200)
1340+
response.body.dump()
1341+
})
1342+
1343+
client.request({
1344+
path: '/',
1345+
method: 'GET',
1346+
headers: {
1347+
'x-my-header': 'foo'
1348+
},
1349+
signal: controller.signal
1350+
}, (err, response) => {
1351+
t.strictEqual(err.name, 'AbortError')
1352+
})
1353+
1354+
client.request({
1355+
path: '/',
1356+
method: 'GET',
1357+
headers: {
1358+
'x-my-header': 'foo'
1359+
}
1360+
}, (err, response) => {
1361+
t.ifError(err)
1362+
t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8')
1363+
t.strictEqual(response.headers['x-custom-h2'], 'hello')
1364+
t.strictEqual(response.statusCode, 200)
1365+
})
1366+
1367+
client.request({
1368+
path: '/',
1369+
method: 'GET',
1370+
headers: {
1371+
'x-my-header': 'foo'
1372+
},
1373+
signal: controller.signal
1374+
}, (err, response) => {
1375+
t.strictEqual(err.name, 'AbortError')
1376+
})
1377+
1378+
controller.abort()
1379+
1380+
await t.completed
1381+
})

0 commit comments

Comments
 (0)