Repository: unjs/h3 (ts)

Issue:
`proxyRequest()` doesn't take into account header casing when overriding headers.
If a header is sent by the client with one casing and it's overridden with another in `proxyRequest` they'll both be sent.

https://github.com/unjs/h3/blob/a3192fb919034bb952f6b75271f50e4732ed7d8e/src/utils/proxy.ts#L42-L49

Pull request description:
fix(proxy): merge overriden headers with different case
<!---
☝️ PR title should follow conventional commits (https://conventionalcommits.org) 
-->

### 🔗 Linked issue

Resolves #450

### ❓ Type of change

<!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply. -->

- [ ] 📖 Documentation (updates to the documentation, readme, or JSdoc annotations)
- [x] 🐞 Bug fix (a non-breaking change that fixes an issue)
- [ ] 👌 Enhancement (improving an existing functionality like performance)
- [ ] ✨ New feature (a non-breaking change that adds functionality)
- [ ] 🧹 Chore (updates to the build process or auxiliary tools and libraries)
- [ ] ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

### 📚 Description

When custom proxy headers are configured, they should override regardless of casing differences. This PR does this by introducing an internal util leveraging `Headers` object.

### 📝 Checklist

<!-- Put an `x` in all the boxes that apply. -->
<!-- If your change requires a documentation PR, please link it appropriately -->
<!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [ ] I have linked an issue or discussion.
- [ ] I have updated the documentation accordingly.


Patch:
diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts
index ba5489d..0576267 100644
--- a/src/utils/proxy.ts
+++ b/src/utils/proxy.ts
@@ -51,22 +51,20 @@ export async function proxyRequest(
   }
 
   // Headers
-  const headers = getProxyRequestHeaders(event);
-  if (opts.fetchOptions?.headers) {
-    Object.assign(headers, opts.fetchOptions.headers);
-  }
-  if (opts.headers) {
-    Object.assign(headers, opts.headers);
-  }
+  const fetchHeaders = mergeHeaders(
+    getProxyRequestHeaders(event),
+    opts.fetchOptions?.headers,
+    opts.headers
+  );
 
   return sendProxy(event, target, {
     ...opts,
     fetchOptions: {
-      headers,
       method,
       body,
       duplex,
       ...opts.fetchOptions,
+      headers: fetchHeaders,
     },
   });
 }
@@ -222,3 +220,22 @@ function rewriteCookieProperty(
     }
   );
 }
+
+function mergeHeaders(
+  defaults: HeadersInit,
+  ...inputs: (HeadersInit | RequestHeaders | undefined)[]
+) {
+  const _inputs = inputs.filter(Boolean) as HeadersInit[];
+  if (_inputs.length === 0) {
+    return defaults;
+  }
+  const merged = new Headers(defaults);
+  for (const input of _inputs) {
+    for (const [key, value] of Object.entries(input!)) {
+      if (value !== undefined) {
+        merged.set(key, value);
+      }
+    }
+  }
+  return merged;
+}


Test patch:
diff --git a/test/proxy.test.ts b/test/proxy.test.ts
index d683a98..536694b 100644
--- a/test/proxy.test.ts
+++ b/test/proxy.test.ts
@@ -87,7 +87,13 @@ describe("", () => {
       app.use(
         "/",
         eventHandler((event) => {
-          return proxyRequest(event, url + "/debug", { fetch });
+          return proxyRequest(event, url + "/debug", {
+            fetch,
+            headers: { "x-custom1": "overriden" },
+            fetchOptions: {
+              headers: { "x-custom2": "overriden" },
+            },
+          });
         })
       );
 
@@ -96,13 +102,19 @@ describe("", () => {
         body: "hello",
         headers: {
           "content-type": "text/custom",
-          "x-custom": "hello",
+          "X-Custom1": "user",
+          "X-Custom2": "user",
+          "X-Custom3": "user",
         },
       }).then((r) => r.json());
 
       const { headers, ...data } = result;
       expect(headers["content-type"]).toEqual("text/custom");
-      expect(headers["x-custom"]).toEqual("hello");
+
+      expect(headers["x-custom1"]).toEqual("overriden");
+      expect(headers["x-custom2"]).toEqual("overriden");
+      expect(headers["x-custom3"]).toEqual("user");
+
       expect(data).toMatchInlineSnapshot(`
         {
           "body": "hello",