Repository: nodejs/undici (JavaScript)

Issue:
RedirectHandler cleans headers too aggressively
## Bug Description

<!-- A clear and concise description of what the bug is. -->

Undici explicitly supports [headers](https://undici.nodejs.org/#/docs/api/Dispatcher?id=parameter-undiciheaders) passed as a [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) object, but the `RedirectHandler` does not account for this object type in its internal [cleanRequestHeaders](https://github.com/nodejs/undici/blob/main/lib/handler/redirect-handler.js#L221) function.

## Reproducible By

<!-- A step by step list on how the bug can be reproduced for examination. -->

1. Configure global dispatcher to use redirect interceptor
2. Use `undici.request` to send a request with headers constructed via `new Headers(...)`

   ```js
   import { Headers, request } from 'undici';

   await request( '/local/test/url', { headers: new Headers({ 'x-my-header': true }) } );
   ```

3. Your local test server should see `'x-my-header': 'true'` in the request headers
4. Your local test server should respond with a `3xx` statusCode and a `location: '/local/test/alt-url'` header
5. Undici should follow the redirect and make a request to the new location
6. Your local test server **WILL NOT** see `'x-my-header': 'true'` in the request headers

Compare to:

1. same as above
2. Send the request this way instead:

   ```js
   await request( '/local/test/url', { headers: { 'x-my-header': true } } );
   ```

3. same as above
4. same as above
5. same as above
6. Your local test server **WILL** see `'x-my-header': 'true'` in the request headers

## Expected Behavior

<!-- A clear and concise description of what you expected to happen. -->

1. Headers on the initial request should be repeated on the redirect request(s) for all supported ways to pass headers

## Logs & Screenshots

<!-- If applicable, add screenshots to help explain your problem, or 
alternatively add your console logs here. -->

I believe I've tracked this down to this logic inside `cleanRequestHeaders`:

```js
// ...
} else if (headers && typeof headers === 'object') {
    for (const key of Object.keys(headers)) {
      if (!shouldRemoveHeader(key, removeContent, unknownOrigin)) {
        ret.push(key, headers[key])
      }
    }
  }
// ...
```

For `headers` constructed with `new Headers(...)`, the `typeof headers === 'object'` condition will return `true`, but `Object.keys(...)` does not behave as expected on a `Headers` object. As a result, this for loop does not push any keys/values into `ret`.

## Environment

<!-- This is just your OS and environment information [e.g. Ubuntu 18.04 LTS,
Node v14.14.0] -->

* macOS 15.0.1 (current)
* Node v20.16.0 (LTS, but two minors behind)

### Additional context



Discussion:
Thanks for reporting! Would you like to send a Pull Request to address this issue? Remember to add unit tests.



Patch:
diff --git a/lib/handler/redirect-handler.js b/lib/handler/redirect-handler.js
index 151cbe2f966..b79c9441c31 100644
--- a/lib/handler/redirect-handler.js
+++ b/lib/handler/redirect-handler.js
@@ -227,9 +227,10 @@ function cleanRequestHeaders (headers, removeContent, unknownOrigin) {
       }
     }
   } else if (headers && typeof headers === 'object') {
-    for (const key of Object.keys(headers)) {
+    const entries = headers instanceof Headers ? headers.entries() : Object.entries(headers)
+    for (const [key, value] of entries) {
       if (!shouldRemoveHeader(key, removeContent, unknownOrigin)) {
-        ret.push(key, headers[key])
+        ret.push(key, value)
       }
     }
   } else {


Test patch:
diff --git a/test/redirect-request.js b/test/redirect-request.js
index c7264bc5faf..50c8931a5f1 100644
--- a/test/redirect-request.js
+++ b/test/redirect-request.js
@@ -227,6 +227,34 @@ for (const factory of [
     await t.completed
   })
 
+  test('should remove Host and request body related headers when following HTTP 303 (Headers)', async t => {
+    t = tspl(t, { plan: 3 })
+
+    const server = await startRedirectingServer()
+
+    const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, {
+      method: 'PATCH',
+      headers: new Headers({
+        'Content-Encoding': 'gzip',
+        'X-Foo1': '1',
+        'X-Foo2': '2',
+        'Content-Type': 'application/json',
+        'X-Foo3': '3',
+        Host: 'localhost',
+        'X-Bar': '4'
+      }),
+      maxRedirections: 10
+    })
+
+    const body = await bodyStream.text()
+
+    t.strictEqual(statusCode, 200)
+    t.ok(!headers.location)
+    t.strictEqual(body, `GET /5 :: host@${server} connection@keep-alive x-bar@4 x-foo1@1 x-foo2@2 x-foo3@3`)
+
+    await t.completed
+  })
+
   test('should follow redirection after a HTTP 307', async t => {
     t = tspl(t, { plan: 3 })