[GH-ISSUE #5876] Magic link error flow with no errorCallbackURL #19000

Closed
opened 2026-04-15 17:45:15 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @septemptus on GitHub (Nov 9, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/5876

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Create a backend with magic link plugin
  2. Create a client with magic link client plugin and a text input for entering the token manually
  3. Type in a random invalid token into the client app and use authClient.magicLink.verify to verify the token with the following call
const { data, error } = await authClient.magicLink.verify({
  query: {
    token,
    // note the missing callbackURL and errorCallbackURL
   },
});
  1. Notice the backend calling itself on /?error=INVALID_TOKEN

Current vs. Expected behavior

What you can notice is that the backend will assume a default errorCallbackURL even though one was not provided and proceed to either
a) throw an error if the path is not found
b) return the result of whatever is on the root path

What I would expect to happen is the verify function resolving the Promise with the error response populated to { code: 'INVALID_TOKEN' } so that I could properly handle it on the client side.

What version of Better Auth are you using?

1.3.34

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:29 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6000",
    "release": "24.6.0",
    "cpuCount": 10,
    "cpuModel": "Apple M1 Pro",
    "totalMemory": "32.00 GB",
    "freeMemory": "0.47 GB"
  },
  "node": {
    "version": "v22.17.1",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.9.2"
  },
  "frameworks": null,
  "databases": [
    {
      "name": "drizzle",
      "version": "^0.44.7"
    },
    {
      "name": "@neondatabase/serverless",
      "version": "^1.0.2"
    }
  ],
  "betterAuth": {
    "version": "^1.3.34",
    "config": {
      "plugins": [
        {
          "name": "magic-link",
          "config": {
            "id": "magic-link",
            "endpoints": {},
            "rateLimit": [
              {
                "window": 60,
                "max": 5
              }
            ]
          }
        }
      ],
      "trustedOrigins": [
        "exp://192.168.0.125:8081"
      ],
      "advanced": {
        "disableOriginCheck": true,
        "database": {
          "generateId": false
        }
      }
    }
  }
}

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

Backend

Auth config (if applicable)

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: 'pg',
    schema: {
      user: userTable,
      session: sessionTable,
      account: accountTable,
      verification: verificationTable,
    },
  }),
  plugins: [
    magicLink({
      sendMagicLink,
    }),
  ],
  trustedOrigins: [process.env.EXPO_GO_URL!],
  advanced: {
    disableOriginCheck: process.env.IS_DEV === 'true',
    database: {
      generateId: false,
    },
  },
});

Additional context

The reason I can't easily make the errorCallbackURL work is because I am using the magic links in a mobile app and I cannot redirect the backend to the mobile app therefore getting the error directly from the verify request is required to properly handle it.

Originally created by @septemptus on GitHub (Nov 9, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/5876 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Create a backend with magic link plugin 2. Create a client with magic link client plugin and a text input for entering the token manually 4. Type in a random *invalid* token into the client app and use `authClient.magicLink.verify` to verify the token with the following call ```typescript const { data, error } = await authClient.magicLink.verify({ query: { token, // note the missing callbackURL and errorCallbackURL }, }); ``` 5. Notice the backend calling itself on `/?error=INVALID_TOKEN` ### Current vs. Expected behavior What you can notice is that the backend will assume a default errorCallbackURL even though one was not provided and proceed to either a) throw an error if the path is not found b) return the result of whatever is on the root path What I would expect to happen is the `verify` function resolving the Promise with the `error` response populated to `{ code: 'INVALID_TOKEN' }` so that I could properly handle it on the client side. ### What version of Better Auth are you using? 1.3.34 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:29 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6000", "release": "24.6.0", "cpuCount": 10, "cpuModel": "Apple M1 Pro", "totalMemory": "32.00 GB", "freeMemory": "0.47 GB" }, "node": { "version": "v22.17.1", "env": "development" }, "packageManager": { "name": "npm", "version": "10.9.2" }, "frameworks": null, "databases": [ { "name": "drizzle", "version": "^0.44.7" }, { "name": "@neondatabase/serverless", "version": "^1.0.2" } ], "betterAuth": { "version": "^1.3.34", "config": { "plugins": [ { "name": "magic-link", "config": { "id": "magic-link", "endpoints": {}, "rateLimit": [ { "window": 60, "max": 5 } ] } } ], "trustedOrigins": [ "exp://192.168.0.125:8081" ], "advanced": { "disableOriginCheck": true, "database": { "generateId": false } } } } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript export const auth = betterAuth({ database: drizzleAdapter(db, { provider: 'pg', schema: { user: userTable, session: sessionTable, account: accountTable, verification: verificationTable, }, }), plugins: [ magicLink({ sendMagicLink, }), ], trustedOrigins: [process.env.EXPO_GO_URL!], advanced: { disableOriginCheck: process.env.IS_DEV === 'true', database: { generateId: false, }, }, }); ``` ### Additional context The reason I can't easily make the `errorCallbackURL` work is because I am using the magic links in a mobile app and I cannot redirect the backend to the mobile app therefore getting the error directly from the `verify` request is required to properly handle it.
GiteaMirror added the lockedenhancement labels 2026-04-15 17:45:15 -05:00
Author
Owner

@better-auth-agent[bot] commented on GitHub (Nov 9, 2025):

Hi there! It looks like when you don’t specify an errorCallbackURL, the magic link plugin is falling back to its default behavior—redirecting to the root (or another default URL) with an error query parameter instead of resolving the verify promise with an error object. This isn’t ideal for mobile apps where you’d rather directly handle the error in your client logic.

To work around this until a fix is in place, you might consider explicitly providing an errorCallbackURL that can be handled gracefully by your mobile app. Alternatively, you could customize your magic link configuration to use a custom error handler so the backend won’t perform an unwanted redirect.

Have you tried these approaches, or would you like additional details on setting up a custom error handler? Tag @better-auth-agent if you would like an answer from the Agent.

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3508864856 --> @better-auth-agent[bot] commented on GitHub (Nov 9, 2025): Hi there! It looks like when you don’t specify an errorCallbackURL, the magic link plugin is falling back to its default behavior—redirecting to the root (or another default URL) with an error query parameter instead of resolving the verify promise with an error object. This isn’t ideal for mobile apps where you’d rather directly handle the error in your client logic. To work around this until a fix is in place, you might consider explicitly providing an errorCallbackURL that can be handled gracefully by your mobile app. Alternatively, you could customize your magic link configuration to use a custom error handler so the backend won’t perform an unwanted redirect. Have you tried these approaches, or would you like additional details on setting up a custom error handler? Tag @better-auth-agent if you would like an answer from the Agent. <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Author
Owner

@dosubot[bot] commented on GitHub (Nov 9, 2025):

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

The current behavior is intentional: when magic link verification fails and no errorCallbackURL is provided, the backend defaults to redirecting to the callbackURL or root path ("/") with an error query parameter (e.g., /?error=INVALID_TOKEN). This means the client receives a redirect, not a direct JSON error response, which is not ideal for mobile apps that can't handle redirects. The only case where a JSON response is returned is on successful verification when no callbackURL is set—errors always trigger a redirect, not a JSON error payload. There is no built-in way to have the verify endpoint return an error object directly to the client in error scenarios without a redirect. Achieving mobile-friendly error handling would require changes to the plugin to support direct error responses in these cases.
See implementation details.

To reply, just mention @dosu.


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

<!-- gh-comment-id:3508867508 --> @dosubot[bot] commented on GitHub (Nov 9, 2025): <!-- Greeting --> Hi @septemptus! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The current behavior is intentional: when magic link verification fails and no errorCallbackURL is provided, the backend defaults to redirecting to the callbackURL or root path ("/") with an error query parameter (e.g., `/?error=INVALID_TOKEN`). This means the client receives a redirect, not a direct JSON error response, which is not ideal for mobile apps that can't handle redirects. The only case where a JSON response is returned is on successful verification when no callbackURL is set—errors always trigger a redirect, not a JSON error payload. There is no built-in way to have the verify endpoint return an error object directly to the client in error scenarios without a redirect. Achieving mobile-friendly error handling would require changes to the plugin to support direct error responses in these cases. [See implementation details](https://github.com/better-auth/better-auth/blob/7067b58d358af0fb28ce4d272513ef21186c253c/packages/better-auth/src/plugins/magic-link/index.ts#L239-L433). <!-- 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/1df6f73e-41b6-4b15-9cd9-ba557fbbe702?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/1df6f73e-41b6-4b15-9cd9-ba557fbbe702?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/1df6f73e-41b6-4b15-9cd9-ba557fbbe702?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/1df6f73e-41b6-4b15-9cd9-ba557fbbe702?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/1df6f73e-41b6-4b15-9cd9-ba557fbbe702?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/1df6f73e-41b6-4b15-9cd9-ba557fbbe702?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/1df6f73e-41b6-4b15-9cd9-ba557fbbe702?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/5876)
Author
Owner

@ping-maxwell commented on GitHub (Feb 11, 2026):

Hello all, we're moving all feature requests or enhancement issues over to Github Discussions.

I've went ahead and created the discussion here:
https://github.com/better-auth/better-auth/discussions/7918

<!-- gh-comment-id:3883101723 --> @ping-maxwell commented on GitHub (Feb 11, 2026): Hello all, we're moving all feature requests or enhancement issues over to Github Discussions. I've went ahead and created the discussion here: https://github.com/better-auth/better-auth/discussions/7918
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#19000