Repository: honojs/hono (TypeScript)

Issue:
hc with dynamic headers
### What is the feature you are proposing?

It would be very handy if you could pass a function to `hc` that gets called on every request and returns the headers. My use case would be to get the JWT from localStorage on every request because the JWT is managed by another library and can change at any time.

```ts
const client = hc<App>("https://example.com", {
  headers: () => ({
    Authorization: `Bearer ${localStorage.getItem("jwt")}`,
  }),
});
```

This would eliminate the need to create a new client whenever the JWT changes.


Discussion:
Thanks <3



Patch:
diff --git a/deno_dist/client/client.ts b/deno_dist/client/client.ts
index 8a6864f87..e6bbb2b7c 100644
--- a/deno_dist/client/client.ts
+++ b/deno_dist/client/client.ts
@@ -35,7 +35,7 @@ class ClientRequestImpl {
     this.url = url
     this.method = method
   }
-  fetch = (
+  fetch = async (
     args?: ValidationTargets & {
       param?: Record<string, string>
     },
@@ -82,7 +82,11 @@ class ClientRequestImpl {
 
     const headerValues: Record<string, string> = {
       ...(args?.header ?? {}),
-      ...(opt?.headers ? opt.headers : {}),
+      ...(typeof opt?.headers === 'function'
+        ? await opt.headers()
+        : opt?.headers
+        ? opt.headers
+        : {}),
     }
 
     if (args?.cookie) {
diff --git a/deno_dist/client/types.ts b/deno_dist/client/types.ts
index 2ed3416fd..31b039792 100644
--- a/deno_dist/client/types.ts
+++ b/deno_dist/client/types.ts
@@ -7,11 +7,13 @@ type HonoRequest = (typeof Hono.prototype)['request']
 
 export type ClientRequestOptions<T = unknown> = keyof T extends never
   ? {
-      headers?: Record<string, string>
+      headers?:
+        | Record<string, string>
+        | (() => Record<string, string> | Promise<Record<string, string>>)
       fetch?: typeof fetch | HonoRequest
     }
   : {
-      headers: T
+      headers: T | (() => T | Promise<T>)
       fetch?: typeof fetch | HonoRequest
     }
 
diff --git a/src/client/client.ts b/src/client/client.ts
index 0d5b52eca..8165c57cc 100644
--- a/src/client/client.ts
+++ b/src/client/client.ts
@@ -35,7 +35,7 @@ class ClientRequestImpl {
     this.url = url
     this.method = method
   }
-  fetch = (
+  fetch = async (
     args?: ValidationTargets & {
       param?: Record<string, string>
     },
@@ -82,7 +82,11 @@ class ClientRequestImpl {
 
     const headerValues: Record<string, string> = {
       ...(args?.header ?? {}),
-      ...(opt?.headers ? opt.headers : {}),
+      ...(typeof opt?.headers === 'function'
+        ? await opt.headers()
+        : opt?.headers
+        ? opt.headers
+        : {}),
     }
 
     if (args?.cookie) {
diff --git a/src/client/types.ts b/src/client/types.ts
index d0503fd42..1a3224a91 100644
--- a/src/client/types.ts
+++ b/src/client/types.ts
@@ -7,11 +7,13 @@ type HonoRequest = (typeof Hono.prototype)['request']
 
 export type ClientRequestOptions<T = unknown> = keyof T extends never
   ? {
-      headers?: Record<string, string>
+      headers?:
+        | Record<string, string>
+        | (() => Record<string, string> | Promise<Record<string, string>>)
       fetch?: typeof fetch | HonoRequest
     }
   : {
-      headers: T
+      headers: T | (() => T | Promise<T>)
       fetch?: typeof fetch | HonoRequest
     }
 


Test patch:
diff --git a/src/client/client.test.ts b/src/client/client.test.ts
index fdfa8a646..c248d9d61 100644
--- a/src/client/client.test.ts
+++ b/src/client/client.test.ts
@@ -3,7 +3,7 @@
 /* eslint-disable @typescript-eslint/ban-ts-comment */
 import { rest } from 'msw'
 import { setupServer } from 'msw/node'
-import { vi, expectTypeOf } from 'vitest'
+import { expectTypeOf, vi } from 'vitest'
 import { Hono } from '../hono'
 import { parse } from '../utils/cookie'
 import type { Equal, Expect } from '../utils/types'
@@ -622,3 +622,55 @@ describe('Client can be awaited', () => {
     expect(awaited).toEqual(client)
   })
 })
+
+describe('Dynamic headers', () => {
+  const app = new Hono()
+
+  const route = app.post('/posts', (c) => {
+    return c.json({
+      requestDynamic: 'dummy',
+    })
+  })
+
+  type AppType = typeof route
+
+  const server = setupServer(
+    rest.post('http://localhost/posts', async (req, res, ctx) => {
+      const requestDynamic = req.headers.get('x-dynamic')
+      const payload = {
+        requestDynamic,
+      }
+      return res(ctx.status(200), ctx.json(payload))
+    })
+  )
+
+  beforeAll(() => server.listen())
+  afterEach(() => server.resetHandlers())
+  afterAll(() => server.close())
+
+  let dynamic = ''
+
+  const client = hc<AppType>('http://localhost', {
+    headers: () => ({ 'x-hono': 'hono', 'x-dynamic': dynamic }),
+  })
+
+  it('Should have "x-dynamic": "one"', async () => {
+    dynamic = 'one'
+
+    const res = await client.posts.$post()
+
+    expect(res.ok).toBe(true)
+    const data = await res.json()
+    expect(data.requestDynamic).toEqual('one')
+  })
+
+  it('Should have "x-dynamic": "two"', async () => {
+    dynamic = 'two'
+
+    const res = await client.posts.$post()
+
+    expect(res.ok).toBe(true)
+    const data = await res.json()
+    expect(data.requestDynamic).toEqual('two')
+  })
+})