From 3df169632c36fd51680fec7a3762c60ea01039ad Mon Sep 17 00:00:00 2001 From: Khafra Date: Fri, 22 Nov 2024 03:59:05 -0500 Subject: [PATCH 1/2] fix: sending formdata bodies with http2 (#3863) (cherry picked from commit e49b5751ddfe726ebc6498f07a4af86de319b691) --- lib/dispatcher/client-h2.js | 15 ++++- test/http2.js | 128 +++++++++++++++++++++++++++++++++++- 2 files changed, 141 insertions(+), 2 deletions(-) diff --git a/lib/dispatcher/client-h2.js b/lib/dispatcher/client-h2.js index 0448fa00736..00084738a1c 100644 --- a/lib/dispatcher/client-h2.js +++ b/lib/dispatcher/client-h2.js @@ -31,6 +31,8 @@ const { const kOpenStreams = Symbol('open streams') +let extractBody + // Experimental let h2ExperimentalWarned = false @@ -260,7 +262,8 @@ function shouldSendContentLength (method) { function writeH2 (client, request) { const session = client[kHTTP2Session] - const { body, method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request + const { method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request + let { body } = request if (upgrade) { util.errorRequest(client, request, new Error('Upgrade not supported for H2')) @@ -381,6 +384,16 @@ function writeH2 (client, request) { let contentLength = util.bodyLength(body) + if (util.isFormDataLike(body)) { + extractBody ??= require('../web/fetch/body.js').extractBody + + const [bodyStream, contentType] = extractBody(body) + headers['content-type'] = contentType + + body = bodyStream.stream + contentLength = bodyStream.length + } + if (contentLength == null) { contentLength = request.contentLength } diff --git a/test/http2.js b/test/http2.js index d6840a1bd15..4dd76e62838 100644 --- a/test/http2.js +++ b/test/http2.js @@ -10,7 +10,7 @@ const { Writable, pipeline, PassThrough, Readable } = require('node:stream') const pem = require('https-pem') -const { Client, Agent } = require('..') +const { Client, Agent, FormData } = require('..') const isGreaterThanv20 = process.versions.node.split('.').map(Number)[0] >= 20 @@ -1450,3 +1450,129 @@ test('#3671 - Graceful close', async (t) => { await t.completed }) + +test('#3753 - Handle GOAWAY Gracefully', async (t) => { + const server = createSecureServer(pem) + let counter = 0 + let session = null + + server.on('session', s => { + session = s + }) + + server.on('stream', (stream) => { + counter++ + + // Due to the nature of the test, we need to ignore the error + // that is thrown when the session is destroyed and stream + // is in-flight + stream.on('error', () => {}) + if (counter === 9 && session != null) { + session.goaway() + stream.end() + } else { + stream.respond({ + 'content-type': 'text/plain', + ':status': 200 + }) + setTimeout(() => { + stream.end('hello world') + }, 150) + } + }) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + pipelining: 2, + allowH2: true + }) + + t = tspl(t, { plan: 30 }) + after(() => client.close()) + after(() => server.close()) + + for (let i = 0; i < 15; i++) { + client.request({ + path: '/', + method: 'GET', + headers: { + 'x-my-header': 'foo' + } + }, (err, response) => { + if (err) { + t.strictEqual(err.message, 'HTTP/2: "GOAWAY" frame received with code 0') + t.strictEqual(err.code, 'UND_ERR_SOCKET') + } else { + t.strictEqual(response.statusCode, 200) + ;(async function () { + let body + try { + body = await response.body.text() + } catch (err) { + t.strictEqual(err.code, 'UND_ERR_SOCKET') + return + } + t.strictEqual(body, 'hello world') + })() + } + }) + } + + await t.completed +}) + +test('#3803 - sending FormData bodies works', async (t) => { + const assert = tspl(t, { plan: 4 }) + + const server = createSecureServer(pem).listen(0) + server.on('stream', async (stream, headers) => { + const contentLength = Number(headers['content-length']) + + assert.ok(!Number.isNaN(contentLength)) + assert.ok(headers['content-type']?.startsWith('multipart/form-data; boundary=')) + + stream.respond({ ':status': 200 }) + + const fd = await new Response(stream, { + headers: { + 'content-type': headers['content-type'] + } + }).formData() + + assert.deepEqual(fd.get('a'), 'b') + assert.deepEqual(fd.get('c').name, 'e.fgh') + + stream.end() + }) + + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.after(async () => { + server.close() + await client.close() + }) + + const fd = new FormData() + fd.set('a', 'b') + fd.set('c', new Blob(['d']), 'e.fgh') + + await client.request({ + path: '/', + method: 'POST', + body: fd + }) + + await assert.completed +}) From 83a8780ff632c108d430daeeea1fe870a4a408f6 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Fri, 22 Nov 2024 10:09:48 +0100 Subject: [PATCH 2/2] fix: bad merge --- test/http2.js | 75 --------------------------------------------------- 1 file changed, 75 deletions(-) diff --git a/test/http2.js b/test/http2.js index 036f4f8284e..7d130e670a9 100644 --- a/test/http2.js +++ b/test/http2.js @@ -1443,81 +1443,6 @@ test('#3671 - Graceful close', async (t) => { await t.completed }) -test('#3753 - Handle GOAWAY Gracefully', async (t) => { - const server = createSecureServer(pem) - let counter = 0 - let session = null - - server.on('session', s => { - session = s - }) - - server.on('stream', (stream) => { - counter++ - - // Due to the nature of the test, we need to ignore the error - // that is thrown when the session is destroyed and stream - // is in-flight - stream.on('error', () => {}) - if (counter === 9 && session != null) { - session.goaway() - stream.end() - } else { - stream.respond({ - 'content-type': 'text/plain', - ':status': 200 - }) - setTimeout(() => { - stream.end('hello world') - }, 150) - } - }) - - server.listen(0) - await once(server, 'listening') - - const client = new Client(`https://localhost:${server.address().port}`, { - connect: { - rejectUnauthorized: false - }, - pipelining: 2, - allowH2: true - }) - - t = tspl(t, { plan: 30 }) - after(() => client.close()) - after(() => server.close()) - - for (let i = 0; i < 15; i++) { - client.request({ - path: '/', - method: 'GET', - headers: { - 'x-my-header': 'foo' - } - }, (err, response) => { - if (err) { - t.strictEqual(err.message, 'HTTP/2: "GOAWAY" frame received with code 0') - t.strictEqual(err.code, 'UND_ERR_SOCKET') - } else { - t.strictEqual(response.statusCode, 200) - ;(async function () { - let body - try { - body = await response.body.text() - } catch (err) { - t.strictEqual(err.code, 'UND_ERR_SOCKET') - return - } - t.strictEqual(body, 'hello world') - })() - } - }) - } - - await t.completed -}) - test('#3803 - sending FormData bodies works', async (t) => { const assert = tspl(t, { plan: 4 })