[GH-ISSUE #723] NextJS 15 Server Actions - Headers handling requiring Request object creation for all auth.api.* calls #8396

Closed
opened 2026-04-13 03:28:08 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @LexiconAlex on GitHub (Dec 2, 2024).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/723

Describe the bug

When using NextJS 15 and server actions, the headers (IP-address and User-agent) don't work by passing headers directly. This affects all auth.api.* calls. The only working solution is to create a new Request object with the headers.

To Reproduce

Steps to reproduce the behavior:

  1. Set up NextJS 15 project with server actions
  2. Try to use any auth.api.* calls with direct headers, for example:
// This doesn't work
await auth.api.signInEmail({ 
  headers: await headers(),
  body: result.data,
});
  1. Observe that it doesn't work
  2. Have to use this workaround instead:
// Current workaround
await auth.api.signInEmail({ 
  request: new Request(env.BETTER_AUTH_URL || "", {
    headers: await headers(),
  }),
  body: result.data,
});

Expected behavior

  1. Should be able to pass headers directly without needing to create a new Request object
  2. The IP address and User-agent should be properly inferred from the headers
  3. Ideally, this functionality should be implemented in the core to handle headers automatically for all auth.api.* calls

Environment

  • OS: All
  • Framework: Next.js 15
  • Type: Server Action implementation

Additional context

  • This issue affects ALL auth.api.* calls, not just signInEmail
  • The issue persists even when just passing headers without creating a new Request object

Potential Solutions

  1. Implement automatic header handling in the core for all auth.api.* calls
  2. Provide a utility function that handles the header transformation internally
  3. Update the API to accept headers directly without requiring Request object creation
Originally created by @LexiconAlex on GitHub (Dec 2, 2024). Original GitHub issue: https://github.com/better-auth/better-auth/issues/723 ## Describe the bug When using NextJS 15 and server actions, the headers (IP-address and User-agent) don't work by passing headers directly. This affects all auth.api.* calls. The only working solution is to create a new Request object with the headers. ## To Reproduce Steps to reproduce the behavior: 1. Set up NextJS 15 project with server actions 2. Try to use any auth.api.* calls with direct headers, for example: ```js // This doesn't work await auth.api.signInEmail({ headers: await headers(), body: result.data, }); ``` 3. Observe that it doesn't work 4. Have to use this workaround instead: ```js // Current workaround await auth.api.signInEmail({ request: new Request(env.BETTER_AUTH_URL || "", { headers: await headers(), }), body: result.data, }); ``` ## Expected behavior 1. Should be able to pass headers directly without needing to create a new Request object 2. The IP address and User-agent should be properly inferred from the headers 3. Ideally, this functionality should be implemented in the core to handle headers automatically for all auth.api.* calls ## Environment - OS: All - Framework: Next.js 15 - Type: Server Action implementation ## Additional context - This issue affects ALL auth.api.* calls, not just signInEmail - The issue persists even when just passing headers without creating a new Request object ## Potential Solutions 1. Implement automatic header handling in the core for all auth.api.* calls 2. Provide a utility function that handles the header transformation internally 3. Update the API to accept headers directly without requiring Request object creation
GiteaMirror added the locked label 2026-04-13 03:28:08 -05:00
Author
Owner

@focux commented on GitHub (Dec 2, 2024):

I found out that the reason it doesn't work it's because you pass all your headers from the Next.js, usually containing compression/encoding headers that make the request not work.

What I did was creating a wrapper to only get the cookie header, which is the only header you need to pass to the backend.

export const getCookieHeaders = async () => {
  const currentHeaders = await headers();
  const cookie = currentHeaders.get("cookie");
  if (!cookie) return;

  return { cookie };
};

export type CookieHeaders = Awaited<ReturnType<typeof getCookieHeaders>>;

And then you would use it like this:

await auth.api.signInEmail({ 
  headers: await getCookieHeaders(),
  body: result.data,
});
<!-- gh-comment-id:2511813531 --> @focux commented on GitHub (Dec 2, 2024): I found out that the reason it doesn't work it's because you pass all your headers from the Next.js, usually containing compression/encoding headers that make the request not work. What I did was creating a wrapper to only get the cookie header, which is the only header you need to pass to the backend. ```ts export const getCookieHeaders = async () => { const currentHeaders = await headers(); const cookie = currentHeaders.get("cookie"); if (!cookie) return; return { cookie }; }; export type CookieHeaders = Awaited<ReturnType<typeof getCookieHeaders>>; ``` And then you would use it like this: ```ts await auth.api.signInEmail({ headers: await getCookieHeaders(), body: result.data, }); ```
Author
Owner

@LexiconAlex commented on GitHub (Dec 2, 2024):

I did not get the IP and user-agent in the session with your solution.

<!-- gh-comment-id:2512382760 --> @LexiconAlex commented on GitHub (Dec 2, 2024): I did not get the IP and user-agent in the session with your solution.
Author
Owner

@focux commented on GitHub (Dec 2, 2024):

Just update the function to pass whatever you want, in your case, this would be something like this:

export const getAuthHeaders = async () => {
  const currentHeaders = await headers();
  const cookie = currentHeaders.get("cookie");
  const userAgent = currentHeaders.get("user-agent");
  const ip =
    currentHeaders.get("x-forwarded-for") ?? currentHeaders.get("x-real-ip");

  const customHeaders: Record<string, string> = {};
  if (cookie) customHeaders.cookie = cookie;
  if (userAgent) customHeaders["user-agent"] = userAgent;
  if (ip) customHeaders["x-real-ip"] = ip;

  return customHeaders;
};
<!-- gh-comment-id:2512435827 --> @focux commented on GitHub (Dec 2, 2024): Just update the function to pass whatever you want, in your case, this would be something like this: ```ts export const getAuthHeaders = async () => { const currentHeaders = await headers(); const cookie = currentHeaders.get("cookie"); const userAgent = currentHeaders.get("user-agent"); const ip = currentHeaders.get("x-forwarded-for") ?? currentHeaders.get("x-real-ip"); const customHeaders: Record<string, string> = {}; if (cookie) customHeaders.cookie = cookie; if (userAgent) customHeaders["user-agent"] = userAgent; if (ip) customHeaders["x-real-ip"] = ip; return customHeaders; }; ```
Author
Owner

@LexiconAlex commented on GitHub (Dec 3, 2024):

It seems like header prop does not get and send the values to the session db.
When I console log your solution i get the user-agent and IP but its not going to the sessions table.

<!-- gh-comment-id:2513824005 --> @LexiconAlex commented on GitHub (Dec 3, 2024): It seems like header prop does not get and send the values to the session db. When I console log your solution i get the user-agent and IP but its not going to the sessions table.
Author
Owner

@Bekacru commented on GitHub (Dec 6, 2024):

This has already been addressed. We have a test here that passes.

b6c543438c/packages/better-auth/src/api/routes/sign-in.test.ts (L24)

<!-- gh-comment-id:2523211535 --> @Bekacru commented on GitHub (Dec 6, 2024): This has already been addressed. We have a test here that passes. https://github.com/better-auth/better-auth/blob/b6c543438c732a0f9b09b21d515f08754c85aa93/packages/better-auth/src/api/routes/sign-in.test.ts#L24
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#8396