From ee6176cd2e09853c868bf5bc1a34bf0500963e4d Mon Sep 17 00:00:00 2001
From: Carlos Fuentes <me@metcoder.dev>
Date: Fri, 22 Nov 2024 12:00:13 +0100
Subject: [PATCH] fix: sending formdata bodies with http2 (#3863) [backport]
 (#3866)

* fix: sending formdata bodies with http2 (#3863)

(cherry picked from commit e49b5751ddfe726ebc6498f07a4af86de319b691)

* fix: bad merge

---------

Co-authored-by: Khafra <maitken033380023@gmail.com>
---
 lib/dispatcher/client-h2.js | 15 ++++++++++-
 test/http2.js               | 53 ++++++++++++++++++++++++++++++++++++-
 2 files changed, 66 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 3f7ab3deb24..7d130e670a9 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
 
@@ -1442,3 +1442,54 @@ test('#3671 - Graceful close', async (t) => {
 
   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
+})