Repository: nodejs/undici

Problem statement:
v7 `RetryAgent` converts header with multiple entries (eg. set-cookie) to `\x00`
## Bug Description

After upgrading from `6.21.0` to `7.1.0` the response `set-cookie` header contains `\x00` instead of actual values.

Expected

```js
{
  server: 'nginx',
  date: 'Mon, 09 Dec 2024 07:11:11 GMT',
  'content-type': 'application/json; charset=utf-8',
  'content-length': '359',
  connection: 'keep-alive',
  'set-cookie': [
    'CloudFront-Policy=xxx; Path=/; Domain=example.com; HTTPOnly',
    'CloudFront-Key-Pair-Id=xxx; Path=/; Domain=example.com; HTTPOnly',
    'CloudFront-Signature=xxx; Path=/; Domain=example.com; HTTPOnly'
  ]
}
```

Received

```js
{
  server: 'nginx',
  date: 'Mon, 09 Dec 2024 07:08:57 GMT',
  'content-type': 'application/json; charset=utf-8',
  'content-length': '364',
  connection: 'keep-alive',
  'set-cookie': '\x00\x00\x00',
}
```

## Reproducible By

<!-- A step by step list on how the bug can be reproduced for examination. -->

## Expected Behavior

<!-- A clear and concise description of what you expected to happen. -->

## Logs & Screenshots

<!-- If applicable, add screenshots to help explain your problem, or 
alternatively add your console logs here. -->

## Environment

<!-- This is just your OS and environment information [e.g. Ubuntu 18.04 LTS,
Node v14.14.0] -->

### Additional context

<!-- Add any other context about the problem here. -->



Hints:
Can you provide an 
[Minimum Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example) to support you better?
Unfortunately I'm unable to reproduce the issue locally. I tried `node:http` and `node:http2` as server and sending multiple `set-cookie` headers works in both cases.

In production I'm fetching from `https://developer.api.autodesk.com` to get signed cookies, see https://aps.autodesk.com/blog/download-derivative-files-using-new-signedcookies-api-without-setting-cookies-first-header

In this case the `set-cookie` header is always `\x00\x00\x00` using v7 but works using v6.

Atm I don't have time to investigate this further.
Unfortunately without reproduction is hard to assess what might be happening; it can be a bad encoding parsing of the signed header while receiving the response, but that can also be due to bad encoding from the source.

Without isolated reproduction is hard to tell what can be going wrong. Happy to have a look if we can isolate the problem

Patch:
diff --git a/lib/handler/wrap-handler.js b/lib/handler/wrap-handler.js
--- a/lib/handler/wrap-handler.js
+++ b/lib/handler/wrap-handler.js
@@ -53,8 +53,7 @@ module.exports = class WrapHandler {
   onRequestUpgrade (controller, statusCode, headers, socket) {
     const rawHeaders = []
     for (const [key, val] of Object.entries(headers)) {
-      // TODO (fix): What if val is Array
-      rawHeaders.push(Buffer.from(key), Buffer.from(val))
+      rawHeaders.push(Buffer.from(key), Array.isArray(val) ? val.map(v => Buffer.from(v)) : Buffer.from(val))
     }
 
     this.#handler.onUpgrade?.(statusCode, rawHeaders, socket)
@@ -63,8 +62,7 @@ module.exports = class WrapHandler {
   onResponseStart (controller, statusCode, headers, statusMessage) {
     const rawHeaders = []
     for (const [key, val] of Object.entries(headers)) {
-      // TODO (fix): What if val is Array
-      rawHeaders.push(Buffer.from(key), Buffer.from(val))
+      rawHeaders.push(Buffer.from(key), Array.isArray(val) ? val.map(v => Buffer.from(v)) : Buffer.from(val))
     }
 
     if (this.#handler.onHeaders?.(statusCode, rawHeaders, () => controller.resume(), statusMessage) === false) {
@@ -81,8 +79,7 @@ module.exports = class WrapHandler {
   onResponseEnd (controller, trailers) {
     const rawTrailers = []
     for (const [key, val] of Object.entries(trailers)) {
-      // TODO (fix): What if val is Array
-      rawTrailers.push(Buffer.from(key), Buffer.from(val))
+      rawTrailers.push(Buffer.from(key), Array.isArray(val) ? val.map(v => Buffer.from(v)) : Buffer.from(val))
     }
 
     this.#handler.onComplete?.(rawTrailers)


Test patch:
diff --git a/test/client-node-max-header-size.js b/test/client-node-max-header-size.js
--- a/test/client-node-max-header-size.js
+++ b/test/client-node-max-header-size.js
@@ -25,7 +25,7 @@ describe("Node.js' --max-http-header-size cli option", () => {
 
   test("respect Node.js' --max-http-header-size", async (t) => {
     t = tspl(t, { plan: 6 })
-    const command = 'node -e "require(\'.\').request(\'http://localhost:' + server.address().port + '\')"'
+    const command = 'node --disable-warning=ExperimentalWarning -e "require(\'.\').request(\'http://localhost:' + server.address().port + '\')"'
 
     exec(`${command} --max-http-header-size=1`, { stdio: 'pipe' }, (err, stdout, stderr) => {
       t.strictEqual(err.code, 1)
@@ -44,7 +44,7 @@ describe("Node.js' --max-http-header-size cli option", () => {
 
   test('--max-http-header-size with Client API', async (t) => {
     t = tspl(t, { plan: 6 })
-    const command = 'node -e "new (require(\'.\').Client)(new URL(\'http://localhost:200\'))"'
+    const command = 'node --disable-warning=ExperimentalWarning -e "new (require(\'.\').Client)(new URL(\'http://localhost:200\'))"'
 
     exec(`${command} --max-http-header-size=0`, { stdio: 'pipe' }, (err, stdout, stderr) => {
       t.strictEqual(err.code, 1)
diff --git a/test/issue-3934.js b/test/issue-3934.js
new file mode 100644
--- /dev/null
+++ b/test/issue-3934.js
@@ -0,0 +1,32 @@
+'use strict'
+
+const { test } = require('node:test')
+const { createServer } = require('node:http')
+const { once } = require('node:events')
+const assert = require('node:assert')
+const { Agent, RetryAgent, request } = require('..')
+
+// https://github.com/nodejs/undici/issues/3934
+test('WrapHandler works with multiple header values', async (t) => {
+  const server = createServer(async (_req, res) => {
+    const headers = [
+      ['set-cookie', 'a'],
+      ['set-cookie', 'b'],
+      ['set-cookie', 'c']
+    ]
+    res.writeHead(200, headers)
+    res.end()
+  }).listen(0)
+
+  await once(server, 'listening')
+  t.after(() => server.close())
+
+  const agent = new Agent()
+  const retryAgent = new RetryAgent(agent)
+
+  const {
+    headers
+  } = await request(`http://localhost:${server.address().port}`, { dispatcher: retryAgent })
+
+  assert.deepStrictEqual(headers['set-cookie'], ['a', 'b', 'c'])
+})