From 72a26c140a830a5e78e77fb50edc8afffb7bf708 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 29 May 2026 11:32:34 +0200 Subject: [PATCH] fix: preserve queued request ordering after stream body errors Signed-off-by: Matteo Collina --- lib/dispatcher/client-h1.js | 5 -- test/node-test/client-errors.js | 106 +++++++++++++------------------- 2 files changed, 43 insertions(+), 68 deletions(-) diff --git a/lib/dispatcher/client-h1.js b/lib/dispatcher/client-h1.js index f1c52fb5f11..d511a1420ef 100644 --- a/lib/dispatcher/client-h1.js +++ b/lib/dispatcher/client-h1.js @@ -1088,11 +1088,6 @@ function writeH1 (client, request) { headers.push('content-type', body.type) } - if (body && typeof body.read === 'function') { - // Try to read EOF in order to get length. - body.read(0) - } - const bodyLength = util.bodyLength(body) contentLength = bodyLength ?? contentLength diff --git a/test/node-test/client-errors.js b/test/node-test/client-errors.js index 90813539d8a..d260f628b85 100644 --- a/test/node-test/client-errors.js +++ b/test/node-test/client-errors.js @@ -122,71 +122,33 @@ test('GET errors and reconnect with pipelining 3', async (t) => { await p.completed }) -function installErrorAndReconnectServer (server, p, { contentLength, trackPostWithPlan }) { - let sawPost = false - let sawGet = false - - server.on('request', (req, res) => { - if (req.method === 'GET') { - if (sawGet) { - req.socket?.destroy() - return - } - - sawGet = true - p.strictEqual('/', req.url) - p.strictEqual('GET', req.method) - res.setHeader('content-type', 'text/plain') - res.end('hello') - return - } - - if (sawPost) { - // Node.js 26 can surface additional POST attempts around the queued GET. - // Tear them down and keep the test focused on the reconnect behavior. - req.resume() - req.socket?.destroy() - return - } - - sawPost = true +function errorAndPipelining (type) { + test(`POST with a ${type} that errors and pipelining 1 should reconnect`, async (t) => { + const p = tspl(t, { plan: 12 }) - if (trackPostWithPlan) { + const server = createServer({ joinDuplicateHeaders: true }) + server.once('request', (req, res) => { p.strictEqual('/', req.url) p.strictEqual('POST', req.method) - p.strictEqual(req.headers['content-length'], contentLength) - } else { - assert.strictEqual('/', req.url) - assert.strictEqual('POST', req.method) - assert.strictEqual(req.headers['content-length'], contentLength) - } + p.strictEqual('42', req.headers['content-length']) - const bufs = [] - req.on('data', (buf) => { - bufs.push(buf) - }) + const bufs = [] + req.on('data', (buf) => { + bufs.push(buf) + }) - req.on('aborted', () => { - // we will abruptly close the connection here - // but this will still end - if (trackPostWithPlan) { + req.on('aborted', () => { + // we will abruptly close the connection here + // but this will still end p.strictEqual('a string', Buffer.concat(bufs).toString('utf8')) - } else { - assert.strictEqual('a string', Buffer.concat(bufs).toString('utf8')) - } - }) - }) -} - -function errorAndPipelining (type) { - test(`POST with a ${type} that errors and pipelining 1 should reconnect`, async (t) => { - const trackPostWithPlan = type !== consts.STREAM - const p = tspl(t, { plan: trackPostWithPlan ? 12 : 8 }) + }) - const server = createServer({ joinDuplicateHeaders: true }) - installErrorAndReconnectServer(server, p, { - contentLength: '42', - trackPostWithPlan + server.once('request', (req, res) => { + p.strictEqual('/', req.url) + p.strictEqual('GET', req.method) + res.setHeader('content-type', 'text/plain') + res.end('hello') + }) }) t.after(closeServerAsPromise(server)) @@ -237,13 +199,31 @@ errorAndPipelining(consts.ASYNC_ITERATOR) function errorAndChunkedEncodingPipelining (type) { test(`POST with chunked encoding, ${type} body that errors and pipelining 1 should reconnect`, async (t) => { - const trackPostWithPlan = type !== consts.STREAM - const p = tspl(t, { plan: trackPostWithPlan ? 12 : 8 }) + const p = tspl(t, { plan: 12 }) const server = createServer({ joinDuplicateHeaders: true }) - installErrorAndReconnectServer(server, p, { - contentLength: undefined, - trackPostWithPlan + server.once('request', (req, res) => { + p.strictEqual('/', req.url) + p.strictEqual('POST', req.method) + p.strictEqual(req.headers['content-length'], undefined) + + const bufs = [] + req.on('data', (buf) => { + bufs.push(buf) + }) + + req.on('aborted', () => { + // we will abruptly close the connection here + // but this will still end + p.strictEqual('a string', Buffer.concat(bufs).toString('utf8')) + }) + + server.once('request', (req, res) => { + p.strictEqual('/', req.url) + p.strictEqual('GET', req.method) + res.setHeader('content-type', 'text/plain') + res.end('hello') + }) }) t.after(closeServerAsPromise(server))