diff --git a/lib/client.js b/lib/client.js index ff23b772909..98adffdf182 100644 --- a/lib/client.js +++ b/lib/client.js @@ -808,6 +808,7 @@ class Parser { .removeListener('close', onSocketClose) client[kSocket] = null + client[kHTTP2Session] = null client[kQueue][client[kRunningIdx]++] = null client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade')) @@ -1421,7 +1422,7 @@ function _resume (client, sync) { return } - if (!socket && !client[kHTTP2Session]) { + if (!socket) { connect(client) return } @@ -1796,7 +1797,25 @@ function writeH2 (client, session, request) { }) stream.once('end', () => { - request.onComplete([]) + // When state is null, it means we haven't consumed body and the stream still do not have + // a state. + // Present specially when using pipeline or stream + if (stream.state?.state == null || stream.state.state < 6) { + request.onComplete([]) + return + } + + // Stream is closed or half-closed-remote (6), decrement counter and cleanup + // It does not have sense to continue working with the stream as we do not + // have yet RST_STREAM support on client-side + h2State.openStreams -= 1 + if (h2State.openStreams === 0) { + session.unref() + } + + const err = new InformationalError('HTTP/2: stream half-closed (remote)') + errorRequest(client, request, err) + util.destroy(stream, err) }) stream.on('data', (chunk) => { diff --git a/test/http2.js b/test/http2.js index 7a183a24552..2daef09bd2c 100644 --- a/test/http2.js +++ b/test/http2.js @@ -13,7 +13,7 @@ const { Client, Agent } = require('..') const isGreaterThanv20 = process.versions.node.split('.').map(Number)[0] >= 20 -plan(24) +plan(25) test('Should support H2 connection', async t => { const body = [] @@ -1243,3 +1243,33 @@ test('The h2 pseudo-headers is not included in the headers', async t => { t.equal(response.statusCode, 200) t.equal(response.headers[':status'], undefined) }) + +test('Should throw informational error on half-closed streams (remote)', async t => { + const server = createSecureServer(pem) + + server.on('stream', (stream, headers) => { + stream.destroy() + }) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.plan(2) + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + await client.request({ + path: '/', + method: 'GET' + }).catch(err => { + t.equal(err.message, 'HTTP/2: stream half-closed (remote)') + t.equal(err.code, 'UND_ERR_INFO') + }) +})