[GH-ISSUE #8676] TypeError: session is null when throwing APIError without message in databaseHooks (i18n + OAuth conflict) #11159

Open
opened 2026-04-13 07:31:16 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @Iulian-Dragomirescu on GitHub (Mar 18, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8676

When throwing an APIError inside databaseHooks.user.create.before during OAuth social sign-in, if you omit the message property and rely on the i18n plugin to inject the translation based on code, the server crashes with a 500:

<-- POST /api/auth/sign-in/social
2026-03-18T16:16:15.681Z ERROR [Better Auth]: APIError
2026-03-18T16:16:15.682Z ERROR [Better Auth]: TypeError 123 | async function setSessionCookie(ctx, session, dontRememberMe, overrides) {
124 |   const dontRememberMeCookie = await ctx.getSignedCookie(ctx.context.authCookies.dontRememberToken.name, ctx.context.secret);
125 |   dontRememberMe = dontRememberMe !== void 0 ? dontRememberMe : !!dontRememberMeCookie;
126 |   const options = ctx.context.authCookies.sessionToken.attributes;
127 |   const maxAge = dontRememberMe ? void 0 : ctx.context.sessionConfig.expiresIn;
128 |   await ctx.setSignedCookie(ctx.context.authCookies.sessionToken.name, session.session.token, ctx.context.secret, {
                                                                            ^
TypeError: null is not an object (evaluating 'session.session')
      at setSessionCookie (lovpli/server/node_modules/.pnpm/better-auth@1.5.5_@prisma+client@7.5.0_prisma@7.5.0_@types+react@19.2.14_react-dom@19.2_be99b374499587e8f178a6de2ba3112a/node_modules/better-auth/dist/cookies/index.mjs:128:71)

# SERVER_ERROR:  123 | async function setSessionCookie(ctx, session, dontRememberMe, overrides) {
124 |   const dontRememberMeCookie = await ctx.getSignedCookie(ctx.context.authCookies.dontRememberToken.name, ctx.context.secret);
125 |   dontRememberMe = dontRememberMe !== void 0 ? dontRememberMe : !!dontRememberMeCookie;
126 |   const options = ctx.context.authCookies.sessionToken.attributes;
127 |   const maxAge = dontRememberMe ? void 0 : ctx.context.sessionConfig.expiresIn;
128 |   await ctx.setSignedCookie(ctx.context.authCookies.sessionToken.name, session.session.token, ctx.context.secret, {
                                                                            ^
TypeError: null is not an object (evaluating 'session.session')
      at setSessionCookie (lovpli/server/node_modules/.pnpm/better-auth@1.5.5_@prisma+client@7.5.0_prisma@7.5.0_@types+react@19.2.14_react-dom@19.2_be99b374499587e8f178a6de2ba3112a/node_modules/better-auth/dist/cookies/index.mjs:128:71)

--> POST /api/auth/sign-in/social 500 256ms

If you include message explicitly, it no longer crashes, but the returned code is OAUTH_LINK_ERROR instead of INVALID_ADDITIONAL_DATA. // see below client response

The same applies to authClient.signUp.email, throwing an APIError without message works correctly there, the i18n plugin picks up the translation and returns the correct INVALID_ADDITIONAL_DATA code without any issues.

Steps to reproduce

Here's the auth config:

import { betterAuth } from "better-auth";
import { i18n } from "@better-auth/i18n";
import { APIError } from "better-auth/api";
 
export const auth = betterAuth({
  plugins: [
    i18n({
      defaultLocale: "en",
      translations: {
        en: { INVALID_ADDITIONAL_DATA: "Invalid additional data." },
      },
    }),
  ],
  databaseHooks: {
    user: {
      create: {
        before: async (user, ctx) => {
          if (ctx?.path.startsWith("/sign-in/social")) {
            const additionalData = schema.safeParse(ctx?.body?.additionalData);
            if (additionalData.error) {
              // ❌ crashes — no message, i18n should provide it
              throw new APIError("BAD_REQUEST", {
                code: "INVALID_ADDITIONAL_DATA",
              }); // in client:  🚀 ~ onSubmit ~ data: null {"status": 500, "statusText": ""}
 
              // ✅ works fine
              // throw new APIError("BAD_REQUEST", {
              //   code: "INVALID_ADDITIONAL_DATA",
              //   message: "Invalid additional data",
              // });
             // in the client this is the response code  ~ onSubmit ~ data: null {"code": "OAUTH_LINK_ERROR", "message": "Invalid additional data", "status": 401, "statusText": ""}, 
//not INVALID_ADDITIONAL_DATA
            }
          }
          return { data: user };
        },
      },
    },
  },
  socialProviders: { google: { /* ... */ } },
});

And the client call:

await authClient.signIn.social({
  provider: "google",
  additionalData: { /* invalid data */ },
});

Expected behavior

The i18n plugin should pick up the code and inject the translated message, returning:

{
  "code": "INVALID_ADDITIONAL_DATA",
  "message": "Invalid additional data."
}

What actually happens

The request crashes before the error response is ever sent. The server tries to set a session cookie even though the error should have short-circuited the flow, then blows up because there's no session.

Worth noting: the same hook works correctly with email/password sign-up and with the idToken flow. It's specific to social OAuth + missing message.

Environment

  • Better Auth: 1.5.5
  • i18n plugin: latest
  • Database: Prisma + PostgreSQL
  • Runtime: Bun 1.2.x
Originally created by @Iulian-Dragomirescu on GitHub (Mar 18, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8676 When throwing an `APIError` inside `databaseHooks.user.create.before` during OAuth social sign-in, if you omit the `message` property and rely on the i18n plugin to inject the translation based on `code`, the server crashes with a 500: ``` <-- POST /api/auth/sign-in/social 2026-03-18T16:16:15.681Z ERROR [Better Auth]: APIError 2026-03-18T16:16:15.682Z ERROR [Better Auth]: TypeError 123 | async function setSessionCookie(ctx, session, dontRememberMe, overrides) { 124 | const dontRememberMeCookie = await ctx.getSignedCookie(ctx.context.authCookies.dontRememberToken.name, ctx.context.secret); 125 | dontRememberMe = dontRememberMe !== void 0 ? dontRememberMe : !!dontRememberMeCookie; 126 | const options = ctx.context.authCookies.sessionToken.attributes; 127 | const maxAge = dontRememberMe ? void 0 : ctx.context.sessionConfig.expiresIn; 128 | await ctx.setSignedCookie(ctx.context.authCookies.sessionToken.name, session.session.token, ctx.context.secret, { ^ TypeError: null is not an object (evaluating 'session.session') at setSessionCookie (lovpli/server/node_modules/.pnpm/better-auth@1.5.5_@prisma+client@7.5.0_prisma@7.5.0_@types+react@19.2.14_react-dom@19.2_be99b374499587e8f178a6de2ba3112a/node_modules/better-auth/dist/cookies/index.mjs:128:71) # SERVER_ERROR: 123 | async function setSessionCookie(ctx, session, dontRememberMe, overrides) { 124 | const dontRememberMeCookie = await ctx.getSignedCookie(ctx.context.authCookies.dontRememberToken.name, ctx.context.secret); 125 | dontRememberMe = dontRememberMe !== void 0 ? dontRememberMe : !!dontRememberMeCookie; 126 | const options = ctx.context.authCookies.sessionToken.attributes; 127 | const maxAge = dontRememberMe ? void 0 : ctx.context.sessionConfig.expiresIn; 128 | await ctx.setSignedCookie(ctx.context.authCookies.sessionToken.name, session.session.token, ctx.context.secret, { ^ TypeError: null is not an object (evaluating 'session.session') at setSessionCookie (lovpli/server/node_modules/.pnpm/better-auth@1.5.5_@prisma+client@7.5.0_prisma@7.5.0_@types+react@19.2.14_react-dom@19.2_be99b374499587e8f178a6de2ba3112a/node_modules/better-auth/dist/cookies/index.mjs:128:71) --> POST /api/auth/sign-in/social 500 256ms ``` If you include `message` explicitly, it no longer crashes, but the returned code is `OAUTH_LINK_ERROR` instead of `INVALID_ADDITIONAL_DATA`. ```// see below client response``` The same applies to `authClient.signUp.email`, throwing an `APIError` without `message` works correctly there, the i18n plugin picks up the translation and returns the correct `INVALID_ADDITIONAL_DATA` code without any issues. ## Steps to reproduce Here's the auth config: ```ts import { betterAuth } from "better-auth"; import { i18n } from "@better-auth/i18n"; import { APIError } from "better-auth/api"; export const auth = betterAuth({ plugins: [ i18n({ defaultLocale: "en", translations: { en: { INVALID_ADDITIONAL_DATA: "Invalid additional data." }, }, }), ], databaseHooks: { user: { create: { before: async (user, ctx) => { if (ctx?.path.startsWith("/sign-in/social")) { const additionalData = schema.safeParse(ctx?.body?.additionalData); if (additionalData.error) { // ❌ crashes — no message, i18n should provide it throw new APIError("BAD_REQUEST", { code: "INVALID_ADDITIONAL_DATA", }); // in client: 🚀 ~ onSubmit ~ data: null {"status": 500, "statusText": ""} // ✅ works fine // throw new APIError("BAD_REQUEST", { // code: "INVALID_ADDITIONAL_DATA", // message: "Invalid additional data", // }); // in the client this is the response code ~ onSubmit ~ data: null {"code": "OAUTH_LINK_ERROR", "message": "Invalid additional data", "status": 401, "statusText": ""}, //not INVALID_ADDITIONAL_DATA } } return { data: user }; }, }, }, }, socialProviders: { google: { /* ... */ } }, }); ``` And the client call: ```ts await authClient.signIn.social({ provider: "google", additionalData: { /* invalid data */ }, }); ``` ## Expected behavior The i18n plugin should pick up the `code` and inject the translated message, returning: ```json { "code": "INVALID_ADDITIONAL_DATA", "message": "Invalid additional data." } ``` ## What actually happens The request crashes before the error response is ever sent. The server tries to set a session cookie even though the error should have short-circuited the flow, then blows up because there's no session. Worth noting: the same hook works correctly with email/password sign-up and with the idToken flow. It's specific to social OAuth + missing `message`. ## Environment - Better Auth: 1.5.5 - i18n plugin: latest - Database: Prisma + PostgreSQL - Runtime: Bun 1.2.x
GiteaMirror added the coreoauthbug labels 2026-04-13 07:31:16 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Mar 18, 2026):

Hi @Iulian-Dragomirescu! I'm Dosu and I’m helping the better-auth team.

This is a known architectural limitation of the i18n plugin. The i18n plugin only registers an after hook, so when you throw an APIError in databaseHooks.user.create.before:

  1. Execution terminates immediately when the error is thrown
  2. runAfterHooks() never runs, so the i18n plugin can't translate the error
  3. The error propagates with an undefined/invalid message property
  4. The OAuth error handling in handleOAuthUserInfo accesses e.message to construct the error object, and when that's undefined, the error handling fails
  5. The flow continues incorrectly, eventually reaching setSessionCookie with a null session

Workaround: Always provide an explicit message in before hooks—don't rely on i18n for translation there:

throw new APIError("BAD_REQUEST", {
  code: "INVALID_ADDITIONAL_DATA",
  message: "Invalid additional data", // Required in before hooks
});

The OAUTH_LINK_ERROR code you see when providing an explicit message is expected—the OAuth callback handler wraps errors with this code during redirect. Your original error message is preserved in the message field.

There's an open issue tracking a proposed fix to modify runBeforeHooks() to catch APIErrors and route them through runAfterHooks() for i18n translation, but it hasn't been implemented yet.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:4083887640 --> @dosubot[bot] commented on GitHub (Mar 18, 2026): <!-- Greeting --> Hi @Iulian-Dragomirescu! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is a known architectural limitation of the i18n plugin. The i18n plugin [only registers an after hook](https://github.com/better-auth/better-auth/issues/8492), so when you throw an `APIError` in `databaseHooks.user.create.before`: 1. Execution terminates immediately when the error is thrown 2. `runAfterHooks()` never runs, so the i18n plugin can't translate the error 3. The error propagates with an undefined/invalid message property 4. The [OAuth error handling in `handleOAuthUserInfo`](https://github.com/better-auth/better-auth/blob/a24bdce5985c203b45fa52ffa7bf232db8f1ec89/packages/better-auth/src/oauth2/link-account.ts#L207) accesses `e.message` to construct the error object, and when that's undefined, the error handling fails 5. The flow continues incorrectly, eventually reaching [`setSessionCookie` with a null session](https://github.com/better-auth/better-auth/blob/a24bdce5985c203b45fa52ffa7bf232db8f1ec89/packages/better-auth/src/cookies/index.ts#L264) **Workaround**: Always provide an explicit `message` in before hooks—don't rely on i18n for translation there: ```ts throw new APIError("BAD_REQUEST", { code: "INVALID_ADDITIONAL_DATA", message: "Invalid additional data", // Required in before hooks }); ``` The `OAUTH_LINK_ERROR` code you see when providing an explicit message is expected—[the OAuth callback handler wraps errors with this code](https://github.com/better-auth/better-auth/blob/a24bdce5985c203b45fa52ffa7bf232db8f1ec89/packages/better-auth/src/api/routes/callback.ts#L97) during redirect. Your original error message is preserved in the `message` field. There's an [open issue tracking a proposed fix](https://github.com/better-auth/better-auth/issues/8492) to modify `runBeforeHooks()` to catch APIErrors and route them through `runAfterHooks()` for i18n translation, but it hasn't been implemented yet. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=brand-link).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/41a33d7e-c29d-490d-9577-bbb4a8f29907?feedback_type=great_response&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/41a33d7e-c29d-490d-9577-bbb4a8f29907?feedback_type=irrelevant_answer&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/41a33d7e-c29d-490d-9577-bbb4a8f29907?feedback_type=incorrect_sources&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/41a33d7e-c29d-490d-9577-bbb4a8f29907?feedback_type=too_verbose&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/41a33d7e-c29d-490d-9577-bbb4a8f29907?feedback_type=hallucination&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/41a33d7e-c29d-490d-9577-bbb4a8f29907?feedback_type=bug_report&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-bug_report) | [Other](https://app.dosu.dev/response-feedback/41a33d7e-c29d-490d-9577-bbb4a8f29907?feedback_type=other&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=chat-badge)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=This%20is%20a%20known%20architectural%20limitation%20of%20the%20i18n%20plugin.%20The%20i18n%20plugin%20%5Bonly%20registers%20an%20after%20hook%5D%28https%3A//github.com/better-auth/better-auth/issues/8492%29%2C%20so%20when%20you%20throw%20an%20%60APIError%60%20in%20%60databaseHooks.user.create.before%60%3A%0A%0A1.%20Execution%20terminates%20immediately%20when%20the%20error%20is%20thrown%0A2.%20%60runAfterHooks%28%29%60%20never%20runs%2C%20so%20the%20i18n%20plugin%20can%27t%20translate%20the%20error%0A3.%20The%20error%20propagates%20with%20an%20undefined/invalid%20message%20property%0A4.%20The%20%5BOAuth%20error%20handling%20in%20%60handleOAuthUserInfo%60%5D%28https%3A//github.com/better-auth/better-auth/blob/a24bdce5985c203b45fa52ffa7bf232db8f1ec89/packages/better-auth/src/oauth2/link-account.ts%23L207%29%20accesses%20%60e.message%60%20to%20construct%20the%20error%20object%2C%20and%20when%20that%27s%20undefined%2C%20the%20error%20handling%20fails%0A5.%20The%20flow%20continues%20incorrectly%2C%20eventually%20reaching%20%5B%60setSessionCookie%60%20with%20a%20null%20session%5D%28https%3A//github.com/better-auth/better-auth/blob/a24bdce5985c203b45fa52ffa7bf232db8f1ec89/packages/better-auth/src/cookies/index.ts%23L264%29%0A%0A%2A%2AWorkaround%2A%2A%3A%20Always%20provide%20an%20explicit%20%60message%60%20in%20before%20hooks%E2%80%94don%27t%20rely%20on%20i18n%20for%20translation%20there%3A%0A%0A%60%60%60ts%0Athrow%20new%20APIError%28%22BAD_REQUEST%22%2C%20%7B%0A%20%20code%3A%20%22INVALID_ADDITIONAL_DATA%22%2C%0A%20%20message%3A%20%22Invalid%20additional%20data%22%2C%20//%20Required%20in%20before%20hooks%0A%7D%29%3B%0A%60%60%60%0A%0AThe%20%60OAUTH_LINK_ERROR%60%20code%20you%20see%20when%20providing%20an%20explicit%20message%20is%20expected%E2%80%94%5Bthe%20OAuth%20callback%20handler%20wraps%20errors%20with%20this%20code%5D%28https%3A//github.com/better-auth/better-auth/blob/a24bdce5985c203b45fa52ffa7bf232db8f1ec89/packages/better-auth/src/api/routes/callback.ts%23L97%29%20during%20redirect.%20Your%20original%20error%20message%20is%20preserved%20in%20the%20%60message%60%20field.%0A%0AThere%27s%20an%20%5Bopen%20issue%20tracking%20a%20proposed%20fix%5D%28https%3A//github.com/better-auth/better-auth/issues/8492%29%20to%20modify%20%60runBeforeHooks%28%29%60%20to%20catch%20APIErrors%20and%20route%20them%20through%20%60runAfterHooks%28%29%60%20for%20i18n%20translation%2C%20but%20it%20hasn%27t%20been%20implemented%20yet.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=join-discord)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/8676)
Author
Owner

@jaydeep-pipaliya commented on GitHub (Apr 9, 2026):

Hi, I'd like to work on this! Could I be assigned?

The root cause is that when an APIError is thrown in databaseHooks.user.create.before during OAuth sign-in, the error is caught but the OAuth callback flow continues to setSessionCookie with a null session, causing the TypeError: null is not an object (evaluating 'session.session') crash. The fix is to properly propagate the error before reaching the cookie-setting code.

<!-- gh-comment-id:4211751102 --> @jaydeep-pipaliya commented on GitHub (Apr 9, 2026): Hi, I'd like to work on this! Could I be assigned? The root cause is that when an `APIError` is thrown in `databaseHooks.user.create.before` during OAuth sign-in, the error is caught but the OAuth callback flow continues to `setSessionCookie` with a null session, causing the `TypeError: null is not an object (evaluating 'session.session')` crash. The fix is to properly propagate the error before reaching the cookie-setting code.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#11159