Repository: honojs/hono (TypeScript)

Issue:
`helper/proxy` not removing `undefined` headers from `ProxyRequestInit`
Using the code from the [Proxy Helper documentation](https://hono.dev/docs/helpers/proxy#proxy-helper)..

```ts
app.get('/proxy/:path', (c) =>
  proxy(`https://example.com/${c.req.param('path')}`, {
    ...c.req,
    headers: {
      Authorization: undefined,
    },
  })
)
```

..the `Authorization` header isn't removed from the proxied request headers


Discussion:

Patch:
diff --git a/src/helper/proxy/index.ts b/src/helper/proxy/index.ts
index 9bfed6f73..96d19f8c2 100644
--- a/src/helper/proxy/index.ts
+++ b/src/helper/proxy/index.ts
@@ -49,6 +49,28 @@ const buildRequestInitFromRequest = (
   }
 }
 
+const preprocessRequestInit = (requestInit: RequestInit): RequestInit => {
+  if (
+    !requestInit.headers ||
+    Array.isArray(requestInit.headers) ||
+    requestInit.headers instanceof Headers
+  ) {
+    return requestInit
+  }
+
+  const headers = new Headers()
+  for (const [key, value] of Object.entries(requestInit.headers)) {
+    if (value == null) {
+      // delete header if value is null or undefined
+      headers.delete(key)
+    } else {
+      headers.set(key, value)
+    }
+  }
+  requestInit.headers = headers
+  return requestInit
+}
+
 /**
  * Fetch API wrapper for proxy.
  * The parameters and return value are the same as for `fetch` (except for the proxy-specific options).
@@ -89,14 +111,10 @@ const buildRequestInitFromRequest = (
 export const proxy: ProxyFetch = async (input, proxyInit) => {
   const { raw, ...requestInit } = proxyInit ?? {}
 
-  const req = new Request(
-    input,
-    // @ts-expect-error `headers` in `requestInit` is not compatible with HeadersInit
-    {
-      ...buildRequestInitFromRequest(raw),
-      ...requestInit,
-    }
-  )
+  const req = new Request(input, {
+    ...buildRequestInitFromRequest(raw),
+    ...preprocessRequestInit(requestInit as RequestInit),
+  })
   req.headers.delete('accept-encoding')
 
   const res = await fetch(req)


Test patch:
diff --git a/src/helper/proxy/index.test.ts b/src/helper/proxy/index.test.ts
index 6d3213873..9c291ff43 100644
--- a/src/helper/proxy/index.test.ts
+++ b/src/helper/proxy/index.test.ts
@@ -5,7 +5,9 @@ describe('Proxy Middleware', () => {
   describe('proxy', () => {
     beforeEach(() => {
       global.fetch = vi.fn().mockImplementation(async (req) => {
-        if (req.url === 'https://example.com/compressed') {
+        if (req.url === 'https://example.com/ok') {
+          return Promise.resolve(new Response('ok'))
+        } else if (req.url === 'https://example.com/compressed') {
           return Promise.resolve(
             new Response('ok', {
               headers: {
@@ -179,5 +181,24 @@ describe('Proxy Middleware', () => {
       expect(res.headers.get('Set-Cookie')).toBeNull()
       expect(res.headers.get('X-Response-Id')).toBe('456')
     })
+
+    it('does not propagate undefined request headers', async () => {
+      const app = new Hono()
+      app.get('/proxy/:path', (c) =>
+        proxy(`https://example.com/${c.req.param('path')}`, {
+          headers: {
+            ...c.req.header(),
+            Authorization: undefined,
+          },
+        })
+      )
+      await app.request('/proxy/ok', {
+        headers: {
+          Authorization: 'Bearer 123',
+        },
+      })
+      const req = (global.fetch as ReturnType<typeof vi.fn>).mock.calls[0][0]
+      expect(req.headers.get('Authorization')).toBeNull()
+    })
   })
 })