the errorCallbackURL isnt workin in the oauth, even after updating the latest version v1.3.11 #1936

Closed
opened 2026-03-13 09:13:31 -05:00 by GiteaMirror · 8 comments
Owner

Originally created by @Rayyan-Balami on GitHub (Sep 16, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

const res = await authClient.signIn.social({
provider: "google",
callbackURL: next || ${APP_DOMAIN}/u,
errorCallbackURL: ${APP_DOMAIN}/auth/login,
});

cases to reproduce:

  1. banned user tries to login using o auth,
  2. Login using oauth, then logout, then go back in browser history where you choosed oauth account.

Current vs. Expected behavior

redirects me to http://localhost:3000/api/auth/error?error=banned&error_description=You%20have%20been%20banned%20from%20this%20application.%20Please%20contact%20support%20if%20you%20believe%20this%20is%20an%20error. (express server)

need to respect the errorCallbackURL in my frontend (react localhost:5173)

What version of Better Auth are you using?

1.3.11

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:34 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T8103",
    "release": "24.6.0",
    "cpuCount": 8,
    "cpuModel": "Apple M1",
    "totalMemory": "8.00 GB",
    "freeMemory": "0.17 GB"
  },
  "node": {
    "version": "v22.17.1",
    "env": "development"
  },
  "packageManager": {
    "name": "pnpm",
    "version": "10.16.1"
  },
  "frameworks": null,
  "databases": null,
  "betterAuth": {
    "version": "Unknown",
    "config": null
  }
}

Which area(s) are affected? (Select all that apply)

Backend, Client

Auth config (if applicable)

import {
  BETTER_AUTH_SECRET,
  BETTER_AUTH_URL,
  CORS_ORIGIN,
  GOOGLE_CLIENT_ID,
  GOOGLE_CLIENT_SECRET,
  IS_PROD,
  MICROSOFT_CLIENT_ID,
  MICROSOFT_CLIENT_SECRET,
  REQUIRE_EMAIL_VERIFICATION,
} from "@/config/constants";
import {
  addProviderEmail,
  ProviderId,
} from "@/features/user/services/addProviderEmail.service";
import { client } from "@/lib/mongo";
import { userRoleEnum } from "@packages/shared/schemas/user";
import { betterAuth } from "better-auth";
import { mongodbAdapter } from "better-auth/adapters/mongodb";
import { admin } from "better-auth/plugins";
import { sendResetPasswordEmail, sendVerificationEmail } from "./resend";

export const auth = betterAuth({
  database: mongodbAdapter(client),
  secret: BETTER_AUTH_SECRET,
  baseURL: BETTER_AUTH_URL,
  trustedOrigins: [CORS_ORIGIN],
  // errorURL: `${CORS_ORIGIN}/auth/error`, used or not still not worked
  user: {
    changeEmail: {
      enabled: true,
    },
  },
  account: {
    accountLinking: {
      enabled: true,
      allowDifferentEmails: true,
      updateUserInfoOnLink: true,
    },
  },
  plugins: [
    admin({
      defaultRole: userRoleEnum.USER,
      adminRoles: [userRoleEnum.ADMIN],
    }),
  ],
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: REQUIRE_EMAIL_VERIFICATION,
    sendResetPassword: async ({ user, url, token }, request) => {
      await sendResetPasswordEmail(user, url);
    },
  },
  emailVerification: {
    sendOnSignUp: true,
    autoSignInAfterVerification: true,
    sendVerificationEmail: async ({ user, url, token }, request) => {
      await sendVerificationEmail(user, url);
    },
  },
  session: {
    expiresIn: 60 * 60 * 24 * 7, // 7 days
    updateAge: 60 * 60 * 24, // 1 day
  },
  advanced: {
    defaultCookieAttributes: {
      sameSite: "lax",
      secure: IS_PROD,
      httpOnly: true,
    },
  },
  socialProviders: {
    google: {
      prompt: "select_account",
      clientId: GOOGLE_CLIENT_ID,
      clientSecret: GOOGLE_CLIENT_SECRET,
      // optional: scope, accessType, etc.
    },
    microsoft: {
      clientId: MICROSOFT_CLIENT_ID,
      clientSecret: MICROSOFT_CLIENT_SECRET,
      tenantId: "common",
      prompt: "select_account",
    },
  },
  databaseHooks: {
    account: {
      create: {
        after: async (account) => {
          // skip if providerId is credentials
          if (account.providerId === "credentials") {
            return;
          }

          // Use service to add provider email
          await addProviderEmail(
            account.id,
            account.providerId as ProviderId,
            account.accessToken || undefined
          );
        },
      },
    },
  },
});

// Use the shared types for consistency
export type Session = typeof auth.$Infer.Session.session;
export type User = typeof auth.$Infer.Session.user;

Additional context

the changelog said Respect errorCallbackURL in failed oauth flows – @frectonz v1.3.11,

Originally created by @Rayyan-Balami on GitHub (Sep 16, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce const res = await authClient.signIn.social({ provider: "google", callbackURL: next || `${APP_DOMAIN}/u`, errorCallbackURL: `${APP_DOMAIN}/auth/login`, }); cases to reproduce: 1. banned user tries to login using o auth, 2. Login using oauth, then logout, then go back in browser history where you choosed oauth account. ### Current vs. Expected behavior redirects me to http://localhost:3000/api/auth/error?error=banned&error_description=You%20have%20been%20banned%20from%20this%20application.%20Please%20contact%20support%20if%20you%20believe%20this%20is%20an%20error. (express server) need to respect the errorCallbackURL in my frontend (react localhost:5173) ### What version of Better Auth are you using? 1.3.11 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:34 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T8103", "release": "24.6.0", "cpuCount": 8, "cpuModel": "Apple M1", "totalMemory": "8.00 GB", "freeMemory": "0.17 GB" }, "node": { "version": "v22.17.1", "env": "development" }, "packageManager": { "name": "pnpm", "version": "10.16.1" }, "frameworks": null, "databases": null, "betterAuth": { "version": "Unknown", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Backend, Client ### Auth config (if applicable) ```typescript import { BETTER_AUTH_SECRET, BETTER_AUTH_URL, CORS_ORIGIN, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, IS_PROD, MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET, REQUIRE_EMAIL_VERIFICATION, } from "@/config/constants"; import { addProviderEmail, ProviderId, } from "@/features/user/services/addProviderEmail.service"; import { client } from "@/lib/mongo"; import { userRoleEnum } from "@packages/shared/schemas/user"; import { betterAuth } from "better-auth"; import { mongodbAdapter } from "better-auth/adapters/mongodb"; import { admin } from "better-auth/plugins"; import { sendResetPasswordEmail, sendVerificationEmail } from "./resend"; export const auth = betterAuth({ database: mongodbAdapter(client), secret: BETTER_AUTH_SECRET, baseURL: BETTER_AUTH_URL, trustedOrigins: [CORS_ORIGIN], // errorURL: `${CORS_ORIGIN}/auth/error`, used or not still not worked user: { changeEmail: { enabled: true, }, }, account: { accountLinking: { enabled: true, allowDifferentEmails: true, updateUserInfoOnLink: true, }, }, plugins: [ admin({ defaultRole: userRoleEnum.USER, adminRoles: [userRoleEnum.ADMIN], }), ], emailAndPassword: { enabled: true, requireEmailVerification: REQUIRE_EMAIL_VERIFICATION, sendResetPassword: async ({ user, url, token }, request) => { await sendResetPasswordEmail(user, url); }, }, emailVerification: { sendOnSignUp: true, autoSignInAfterVerification: true, sendVerificationEmail: async ({ user, url, token }, request) => { await sendVerificationEmail(user, url); }, }, session: { expiresIn: 60 * 60 * 24 * 7, // 7 days updateAge: 60 * 60 * 24, // 1 day }, advanced: { defaultCookieAttributes: { sameSite: "lax", secure: IS_PROD, httpOnly: true, }, }, socialProviders: { google: { prompt: "select_account", clientId: GOOGLE_CLIENT_ID, clientSecret: GOOGLE_CLIENT_SECRET, // optional: scope, accessType, etc. }, microsoft: { clientId: MICROSOFT_CLIENT_ID, clientSecret: MICROSOFT_CLIENT_SECRET, tenantId: "common", prompt: "select_account", }, }, databaseHooks: { account: { create: { after: async (account) => { // skip if providerId is credentials if (account.providerId === "credentials") { return; } // Use service to add provider email await addProviderEmail( account.id, account.providerId as ProviderId, account.accessToken || undefined ); }, }, }, }, }); // Use the shared types for consistency export type Session = typeof auth.$Infer.Session.session; export type User = typeof auth.$Infer.Session.user; ``` ### Additional context the changelog said Respect errorCallbackURL in failed oauth flows – @frectonz [v1.3.11](https://www.better-auth.com/changelogs#v1.3.11),
GiteaMirror added the expressbug labels 2026-03-13 09:13:31 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Sep 16, 2025):

The errorCallbackURL should be respected in OAuth error flows in Better Auth v1.3.11, but there are a few common pitfalls that can cause it to fall back to the backend error page instead:

  • Make sure your frontend domain (e.g., http://localhost:5173) is included in the trustedOrigins array in your Better Auth config. If it's missing, redirects to your frontend will be blocked and you'll see the backend error page instead.
  • Always use a full absolute URL for errorCallbackURL (e.g., http://localhost:5173/auth/login), not a relative path.
  • The errorCallbackURL is passed as errorURL in the OAuth state and is used for error redirects if present and valid; otherwise, it falls back to the backend error URL (like /api/auth/error) see code logic.
  • If the error occurs due to browser navigation history or the OAuth state is lost/corrupted, the errorCallbackURL may not be available and the backend fallback will be used see related discussion.

Double-check that your trustedOrigins includes your frontend, and that you are passing a full absolute errorCallbackURL. If you are, and the issue persists only in certain edge cases (like browser back navigation), it may be due to the OAuth state not being preserved in those flows. This is a known limitation in many OAuth implementations.

If you want to ensure the redirect always goes to your frontend, you can also set the errorURL option in your Better Auth config to your frontend error page as a fallback.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

@dosubot[bot] commented on GitHub (Sep 16, 2025): <!-- Answer --> The errorCallbackURL should be respected in OAuth error flows in Better Auth v1.3.11, but there are a few common pitfalls that can cause it to fall back to the backend error page instead: - Make sure your frontend domain (e.g., http://localhost:5173) is included in the trustedOrigins array in your Better Auth config. If it's missing, redirects to your frontend will be blocked and you'll see the backend error page instead. - Always use a full absolute URL for errorCallbackURL (e.g., http://localhost:5173/auth/login), not a relative path. - The errorCallbackURL is passed as errorURL in the OAuth state and is used for error redirects if present and valid; otherwise, it falls back to the backend error URL (like /api/auth/error) [see code logic](https://github.com/better-auth/better-auth/blob/4df178463eeeced7972c9a59f902b4bbf55db30b/packages/better-auth/src/plugins/generic-oauth/index.ts). - If the error occurs due to browser navigation history or the OAuth state is lost/corrupted, the errorCallbackURL may not be available and the backend fallback will be used [see related discussion](https://github.com/better-auth/better-auth/issues/3289). Double-check that your trustedOrigins includes your frontend, and that you are passing a full absolute errorCallbackURL. If you are, and the issue persists only in certain edge cases (like browser back navigation), it may be due to the OAuth state not being preserved in those flows. This is a known limitation in many OAuth implementations. If you want to ensure the redirect always goes to your frontend, you can also set the errorURL option in your Better Auth config to your frontend error page as a fallback. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/0668f212-0286-4666-99e2-e0ec338e28ce?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/0668f212-0286-4666-99e2-e0ec338e28ce?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/0668f212-0286-4666-99e2-e0ec338e28ce?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/0668f212-0286-4666-99e2-e0ec338e28ce?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/0668f212-0286-4666-99e2-e0ec338e28ce?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/0668f212-0286-4666-99e2-e0ec338e28ce?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/0668f212-0286-4666-99e2-e0ec338e28ce?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&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/4694)
Author
Owner

@Rayyan-Balami commented on GitHub (Sep 16, 2025):

@dosu
export const auth = betterAuth({
database: mongodbAdapter(client),
secret: BETTER_AUTH_SECRET,
baseURL: BETTER_AUTH_URL,
trustedOrigins: [CORS_ORIGIN],
...
})

i have the trusted origin in the Better Auth config,

value of CORS_ORIGIN=http://localhost:5173

@Rayyan-Balami commented on GitHub (Sep 16, 2025): @dosu export const auth = betterAuth({ database: mongodbAdapter(client), secret: BETTER_AUTH_SECRET, baseURL: BETTER_AUTH_URL, trustedOrigins: [CORS_ORIGIN], ... }) i have the trusted origin in the Better Auth config, value of CORS_ORIGIN=http://localhost:5173
Author
Owner

@dosubot[bot] commented on GitHub (Sep 16, 2025):

Your trustedOrigins config looks correct, but there are a few reasons why errorCallbackURL might still not be respected:

  • The errorCallbackURL is only validated and stored in the OAuth state during the initial sign-in POST request. If the state is lost (for example, due to browser navigation, session expiry, or state mismatch), or if the initial validation fails, the redirect will fall back to the backend error URL instead of your frontend URL. This is why you might see the backend error page in some flows, especially after using browser history or in edge cases where the OAuth state isn't preserved see code reference.

  • Make sure errorCallbackURL is always passed as a full absolute URL (e.g., http://localhost:5173/auth/login) in every signIn.social call, and that it matches exactly what's in trustedOrigins.

  • If you want to guarantee the redirect always goes to your frontend, you can also set the errorURL option in your Better Auth config as a fallback.

If you still see backend error pages in specific scenarios, it's likely due to OAuth state loss or a mismatch during the flow, not a problem with your trustedOrigins config see state handling logic.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

@dosubot[bot] commented on GitHub (Sep 16, 2025): <!-- Answer --> Your trustedOrigins config looks correct, but there are a few reasons why errorCallbackURL might still not be respected: - The errorCallbackURL is only validated and stored in the OAuth state during the initial sign-in POST request. If the state is lost (for example, due to browser navigation, session expiry, or state mismatch), or if the initial validation fails, the redirect will fall back to the backend error URL instead of your frontend URL. This is why you might see the backend error page in some flows, especially after using browser history or in edge cases where the OAuth state isn't preserved [see code reference](https://github.com/better-auth/better-auth/blob/4df178463eeeced7972c9a59f902b4bbf55db30b/packages/better-auth/src/plugins/generic-oauth/index.ts). - Make sure errorCallbackURL is always passed as a full absolute URL (e.g., http://localhost:5173/auth/login) in every signIn.social call, and that it matches exactly what's in trustedOrigins. - If you want to guarantee the redirect always goes to your frontend, you can also set the errorURL option in your Better Auth config as a fallback. If you still see backend error pages in specific scenarios, it's likely due to OAuth state loss or a mismatch during the flow, not a problem with your trustedOrigins config [see state handling logic](https://github.com/better-auth/better-auth/blob/4df178463eeeced7972c9a59f902b4bbf55db30b/packages/better-auth/src/oauth2/state.ts). <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/fb97c85d-e19e-4fd0-b813-ca3b8692f010?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/fb97c85d-e19e-4fd0-b813-ca3b8692f010?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/fb97c85d-e19e-4fd0-b813-ca3b8692f010?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/fb97c85d-e19e-4fd0-b813-ca3b8692f010?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/fb97c85d-e19e-4fd0-b813-ca3b8692f010?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/fb97c85d-e19e-4fd0-b813-ca3b8692f010?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/fb97c85d-e19e-4fd0-b813-ca3b8692f010?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&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/4694)
Author
Owner

@Rayyan-Balami commented on GitHub (Sep 16, 2025):

i did the same with linkaccount,
mutationFn: async (data: { provider: string; callbackURL: string; errorCallbackURL: string }) => {
return await authClient.linkSocial(data);
},
....

const res = await linkAccount.mutateAsync({
provider,
callbackURL: window.location.href,
errorCallbackURL: window.location.href,
});

in here the errorCallbackURL worked, but not in oauth login @dosu

@Rayyan-Balami commented on GitHub (Sep 16, 2025): i did the same with linkaccount, mutationFn: async (data: { provider: string; callbackURL: string; errorCallbackURL: string }) => { return await authClient.linkSocial(data); }, .... const res = await linkAccount.mutateAsync({ provider, callbackURL: window.location.href, errorCallbackURL: window.location.href, }); in here the errorCallbackURL worked, but not in oauth login @dosu
Author
Owner

@dosubot[bot] commented on GitHub (Sep 16, 2025):

This happens because both linkSocial and signIn.social store errorCallbackURL in the OAuth state, but the login flow (signIn.social) is more prone to state loss or mismatch—especially after logout or browser back navigation. If the state is missing, expired, or doesn't match, Better Auth falls back to the backend error URL instead of your errorCallbackURL, even if you passed it correctly. The linkSocial flow is tied to an active session, which helps preserve the state and makes errorCallbackURL more reliable in that context.

To reduce issues, always use a stable, absolute URL for errorCallbackURL (not a dynamic window.location.href that might change with navigation), and make sure your frontend domain is in trustedOrigins. If you want a guaranteed frontend redirect on error, set the errorURL option in your Better Auth config as a fallback. But even then, browser navigation edge cases can still break state in OAuth login flows, which is a limitation of how OAuth state works in general (source).

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

@dosubot[bot] commented on GitHub (Sep 16, 2025): <!-- Answer --> This happens because both linkSocial and signIn.social store errorCallbackURL in the OAuth state, but the login flow (signIn.social) is more prone to state loss or mismatch—especially after logout or browser back navigation. If the state is missing, expired, or doesn't match, Better Auth falls back to the backend error URL instead of your errorCallbackURL, even if you passed it correctly. The linkSocial flow is tied to an active session, which helps preserve the state and makes errorCallbackURL more reliable in that context. To reduce issues, always use a stable, absolute URL for errorCallbackURL (not a dynamic window.location.href that might change with navigation), and make sure your frontend domain is in trustedOrigins. If you want a guaranteed frontend redirect on error, set the errorURL option in your Better Auth config as a fallback. But even then, browser navigation edge cases can still break state in OAuth login flows, which is a limitation of how OAuth state works in general ([source](https://github.com/better-auth/better-auth/blob/4df178463eeeced7972c9a59f902b4bbf55db30b/packages/better-auth/src/plugins/generic-oauth/index.ts)). <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/16ea8440-600c-4662-bdf1-0948bfff2ff9?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/16ea8440-600c-4662-bdf1-0948bfff2ff9?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/16ea8440-600c-4662-bdf1-0948bfff2ff9?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/16ea8440-600c-4662-bdf1-0948bfff2ff9?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/16ea8440-600c-4662-bdf1-0948bfff2ff9?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/16ea8440-600c-4662-bdf1-0948bfff2ff9?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/16ea8440-600c-4662-bdf1-0948bfff2ff9?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&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/4694)
Author
Owner

@Rayyan-Balami commented on GitHub (Sep 16, 2025):

what might be the solutions / workarounds? @dosu

@Rayyan-Balami commented on GitHub (Sep 16, 2025): what might be the solutions / workarounds? @dosu
Author
Owner

@dosubot[bot] commented on GitHub (Sep 16, 2025):

Here are the most reliable solutions and workarounds for making errorCallbackURL work in Better Auth OAuth login flows:

  • Always use a full, absolute URL for errorCallbackURL and make sure your frontend domain is listed in trustedOrigins.
  • Set the errorURL option in your Better Auth config to your frontend error page (e.g., errorURL: "http://localhost:5173/auth/error"). This acts as a global fallback if errorCallbackURL is missing or the OAuth state is lost.
  • Avoid passing dynamic URLs like window.location.href for errorCallbackURL, since navigation changes can break state.
  • Double-check that your OAuth provider (e.g., Google Cloud Console) has the correct redirect URIs configured, matching your backend callback endpoint (e.g., http://localhost:3000/api/auth/callback/googlesee code reference.
  • If you use service workers (like Serwist), they can interfere with OAuth state; try disabling them during development if you see state mismatch errors see related issue.

Limitations: If the OAuth state is lost (due to expiry, browser navigation, or other reasons), Better Auth will always fall back to the global errorURL or backend error page. There are no hidden hooks or advanced config options to override this behavior see implementation.

For best UX, make your frontend error page able to parse error query params so users see meaningful messages even if the redirect falls back to the global errorURL.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

@dosubot[bot] commented on GitHub (Sep 16, 2025): <!-- Answer --> Here are the most reliable solutions and workarounds for making errorCallbackURL work in Better Auth OAuth login flows: - Always use a full, absolute URL for errorCallbackURL and make sure your frontend domain is listed in trustedOrigins. - Set the errorURL option in your Better Auth config to your frontend error page (e.g., `errorURL: "http://localhost:5173/auth/error"`). This acts as a global fallback if errorCallbackURL is missing or the OAuth state is lost. - Avoid passing dynamic URLs like `window.location.href` for errorCallbackURL, since navigation changes can break state. - Double-check that your OAuth provider (e.g., Google Cloud Console) has the correct redirect URIs configured, matching your backend callback endpoint (e.g., `http://localhost:3000/api/auth/callback/google`) [see code reference](https://github.com/better-auth/better-auth/blob/4df178463eeeced7972c9a59f902b4bbf55db30b/packages/better-auth/src/oauth2/state.ts). - If you use service workers (like Serwist), they can interfere with OAuth state; try disabling them during development if you see state mismatch errors [see related issue](https://github.com/better-auth/better-auth/issues/3062). Limitations: If the OAuth state is lost (due to expiry, browser navigation, or other reasons), Better Auth will always fall back to the global errorURL or backend error page. There are no hidden hooks or advanced config options to override this behavior [see implementation](https://github.com/better-auth/better-auth/blob/4df178463eeeced7972c9a59f902b4bbf55db30b/packages/better-auth/src/plugins/generic-oauth/index.ts). For best UX, make your frontend error page able to parse error query params so users see meaningful messages even if the redirect falls back to the global errorURL. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/85fa0501-b5eb-4b09-9d0c-53a96df2631e?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/85fa0501-b5eb-4b09-9d0c-53a96df2631e?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/85fa0501-b5eb-4b09-9d0c-53a96df2631e?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/85fa0501-b5eb-4b09-9d0c-53a96df2631e?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/85fa0501-b5eb-4b09-9d0c-53a96df2631e?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/85fa0501-b5eb-4b09-9d0c-53a96df2631e?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/85fa0501-b5eb-4b09-9d0c-53a96df2631e?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&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/4694)
Author
Owner

@ping-maxwell commented on GitHub (Oct 3, 2025):

When you go through the oauth flow, internally it will have your errorCallbackURL set as a state which is stored in your DB.
Upon hitting the /api/auth/callback endpoint the logic will find that value in the DB then consume it (thus deleting it).

The admin plugin intercepts whenever a session is about to be created and then throws a redirect to the error page at that moment if it detects it's a banned user creating a session. Since that state is already consumed in the previous steps it's unable to derive the errorCallbackURL.

Right now as a solution you should set onAPIError.errorURL in your auth config to the error page to display your ban message.

@ping-maxwell commented on GitHub (Oct 3, 2025): When you go through the oauth flow, internally it will have your errorCallbackURL set as a `state` which is stored in your DB. Upon hitting the `/api/auth/callback` endpoint the logic will find that value in the DB then consume it (thus deleting it). The admin plugin intercepts whenever a session is about to be created and then throws a redirect to the error page at that moment if it detects it's a banned user creating a session. Since that state is already consumed in the previous steps it's unable to derive the errorCallbackURL. Right now as a solution you should set `onAPIError.errorURL` in your auth config to the error page to display your ban message.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1936