Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions lib/dispatcher/client-h2.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,10 @@ function completeRequest (client, request, resetPendingIdx = false) {
const queue = client[kQueue]
const runningIdx = client[kRunningIdx]

// In-order completion: advance the running index instead of splicing.
// The client's resume loop compacts the queue once the index grows.
// In-order completion: clear the request and advance without splicing.
// The client's resume loop compacts cleared slots once the index grows.
if (runningIdx < client[kPendingIdx] && queue[runningIdx] === request) {
queue[runningIdx] = null
client[kRunningIdx] = runningIdx + 1
return
}
Expand Down Expand Up @@ -686,9 +687,9 @@ function completeRequestStream () {

if (state.pendingEnd && !state.request.aborted && !state.request.completed) {
state.request.onResponseEnd(state.trailers || {})
finalizeRequest(state)
}

finalizeRequest(state)
closeStreamSession(this)
this[kRequestStreamState] = null
}
Expand Down
71 changes: 71 additions & 0 deletions test/http2-late-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,74 @@ test('Should remove request-owned http2 stream listeners after completion', asyn

await t.completed
})

test('Should finalize an already-aborted request when its stream closes', async (t) => {
t = tspl(t, { plan: 4 })

const http2 = require('node:http2')
const originalConnect = http2.connect

const stream = new FakeStream()
const session = new FakeSession(stream)

http2.connect = function connectStub () {
return session
}

after(() => {
http2.connect = originalConnect
})

const client = {
[kUrl]: new URL('https://localhost'),
[kSocket]: null,
[kMaxConcurrentStreams]: 100,
[kHTTP2InitialWindowSize]: null,
[kHTTP2ConnectionWindowSize]: null,
[kBodyTimeout]: 30_000,
[kStrictContentLength]: true,
[kQueue]: [],
[kRunningIdx]: 0,
[kPendingIdx]: 0,
[kRunning]: 1,
[kPingInterval]: 0,
[kOnError] (err) {
t.ifError(err)
},
[kResume] () {},
emit () {},
destroyed: false
}

const context = connectH2(client, new FakeSocket())
const expectedError = new Error('aborted')
let onCompleteCalls = 0

const request = new Request('https://localhost', {
path: '/',
method: 'GET',
headers: {}
}, {
onRequestStart () {},
onResponseStart () {},
onResponseData () {},
onResponseEnd () {
onCompleteCalls++
},
onResponseError (_controller, err) {
t.strictEqual(err, expectedError)
}
})

client[kQueue].push(request)
client[kPendingIdx] = 1

t.ok(context.write(request))
request.onResponseError(expectedError)
stream.emit('close')

t.strictEqual(onCompleteCalls, 0)
t.strictEqual(client[kQueue][0], null)

await t.completed
})
Loading