[GH-ISSUE #9105] signInMagicLink throw Invalid URL when using baseURL: { .... } #19907

Closed
opened 2026-04-15 19:16:23 -05:00 by GiteaMirror · 4 comments
Owner

Originally created by @j-fdion on GitHub (Apr 10, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/9105

Originally assigned to: @bytaesu on GitHub.

Is this suited for github?

  • Yes, this is suited for github

Reproduction

  1. Configure betterAuth with:
import { betterAuth } from "better-auth"

const LOCAL_DEV_ORIGINS = [
  'localhost:3000',
  'localhost:3010',
]

export const auth = betterAuth({
  baseURL: {
    allowedHosts: [...LOCAL_DEV_ORIGINS],
    protocol: process.env.NODE_ENV === 'development' ? 'http' : 'https',
  },
});
  1. use magic link:
    await auth.api.signInMagicLink({
      body: { email, callbackURL: '/' },
      headers: request.headers,
    })

Current vs. Expected behavior

signInMagicLink should not throw

What version of Better Auth are you using?

1.6.0

System info

{
  "system": {
    "platform": "win32",
    "arch": "x64",
    "version": "Windows 11 Pro",
    "release": "10.0.26200",
    "cpuCount": 16,
    "cpuModel": "AMD Ryzen 7 9800X3D 8-Core Processor           ",
    "totalMemory": "31.06 GB",
    "freeMemory": "13.24 GB"
  },
  "node": {
    "version": "v24.6.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.5.1"
  },
  "frameworks": [
    {
      "name": "react",
      "version": "^19.2.5"
    }
  ],
  "databases": [
    {
      "name": "pg",
      "version": "^8.20.0"
    },
    {
      "name": "drizzle",
      "version": "^0.45.2"
    }
  ],
  "betterAuth": {
    "version": "^1.6.2",
    "config": {
      "baseURL": {
        "allowedHosts": [
          "localhost:3000",
          "localhost:3010",
          "127.0.0.1:3000",
          "*.railway.app"
        ],
        "protocol": "https"
      },
      "advanced": {
        "cookiePrefix": "gcf-paie",
        "crossSubDomainCookies": {
          "enabled": true
        }
      },
      "account": {
        "accountLinking": {
          "allowDifferentEmails": true,
          "trustedProviders": [
            "scheduler"
          ]
        }
      },
      "user": {
        "additionalFields": {
          "matricule": {
            "type": "string",
            "required": false,
            "nullable": true,
            "input": false,
            "returned": true
          },
          "approvedAt": {
            "type": "date",
            "required": false,
            "nullable": true,
            "input": false,
            "returned": true
          }
        }
      },
      "telemetry": {
        "enabled": true,
        "debug": false
      },
      "logger": {
        "disabled": false,
        "disableColors": false,
        "level": "debug"
      },
      "plugins": [
        {
          "name": "magic-link",
          "config": {
            "id": "magic-link",
            "version": "1.6.0",
            "endpoints": {},
            "rateLimit": [
              {
                "window": 60,
                "max": 5
              }
            ],
            "options": {}
          }
        },
        {
          "name": "admin",
          "config": {
            "id": "admin",
            "version": "1.6.0",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {},
            "$ERROR_CODES": {
              "FAILED_TO_CREATE_USER": {
                "code": "FAILED_TO_CREATE_USER",
                "message": "Failed to create user"
              },
              "USER_ALREADY_EXISTS": {
                "code": "USER_ALREADY_EXISTS",
                "message": "User already exists."
              },
              "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": {
                "code": "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL",
                "message": "User already exists. Use another email."
              },
              "YOU_CANNOT_BAN_YOURSELF": {
                "code": "YOU_CANNOT_BAN_YOURSELF",
                "message": "You cannot ban yourself"
              },
              "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE",
                "message": "You are not allowed to change users role"
              },
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS",
                "message": "You are not allowed to create users"
              },
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS",
                "message": "You are not allowed to list users"
              },
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS",
                "message": "You are not allowed to list users sessions"
              },
              "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS",
                "message": "You are not allowed to ban users"
              },
              "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS",
                "message": "You are not allowed to impersonate users"
              },
              "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS",
                "message": "You are not allowed to revoke users sessions"
              },
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS",
                "message": "You are not allowed to delete users"
              },
              "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD",
                "message": "You are not allowed to set users password"
              },
              "BANNED_USER": {
                "code": "BANNED_USER",
                "message": "You have been banned from this application"
              },
              "YOU_ARE_NOT_ALLOWED_TO_GET_USER": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_GET_USER",
                "message": "You are not allowed to get user"
              },
              "NO_DATA_TO_UPDATE": {
                "code": "NO_DATA_TO_UPDATE",
                "message": "No data to update"
              },
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS",
                "message": "You are not allowed to update users"
              },
              "YOU_CANNOT_REMOVE_YOURSELF": {
                "code": "YOU_CANNOT_REMOVE_YOURSELF",
                "message": "You cannot remove yourself"
              },
              "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE",
                "message": "You are not allowed to set a non-existent role value"
              },
              "YOU_CANNOT_IMPERSONATE_ADMINS": {
                "code": "YOU_CANNOT_IMPERSONATE_ADMINS",
                "message": "You cannot impersonate admins"
              },
              "INVALID_ROLE_TYPE": {
                "code": "INVALID_ROLE_TYPE",
                "message": "Invalid role type"
              }
            },
            "schema": {
              "user": {
                "fields": {
                  "role": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banned": {
                    "type": "boolean",
                    "defaultValue": false,
                    "required": false,
                    "input": false
                  },
                  "banReason": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banExpires": {
                    "type": "date",
                    "required": false,
                    "input": false
                  }
                }
              },
              "session": {
                "fields": {
                  "impersonatedBy": {
                    "type": "string",
                    "required": false
                  }
                }
              }
            },
            "options": {
              "ac": {
                "statements": {
                  "user": [
                    "create",
                    "list",
                    "set-role",
                    "ban",
                    "impersonate",
                    "impersonate-admins",
                    "delete",
                    "set-password",
                    "get",
                    "update"
                  ],
                  "session": [
                    "list",
                    "revoke",
                    "delete"
                  ],
                  "project": [
                    "read",
                    "manage"
                  ],
                  "validation": [
                    "review",
                    "finalize",
                    "report"
                  ],
                  "app": [
                    "admin-access",
                    "bypass-approval"
                  ],
                  "role": [
                    "manage-sensitive"
                  ]
                }
              },
              "roles": {
                "user": {
                  "statements": {
                    "project": [
                      "read"
                    ]
                  }
                },
                "supervisor": {
                  "statements": {
                    "project": [
                      "read",
                      "manage"
                    ],
                    "validation": [
                      "review",
                      "report"
                    ]
                  }
                },
                "admin": {
                  "statements": {
                    "user": [
                      "create",
                      "list",
                      "set-role",
                      "ban",
                      "impersonate",
                      "delete",
                      "set-password",
                      "get",
                      "update"
                    ],
                    "session": [
                      "list",
                      "revoke",
                      "delete"
                    ],
                    "project": [
                      "read",
                      "manage"
                    ],
                    "validation": [
                      "review",
                      "finalize",
                      "report"
                    ],
                    "app": [
                      "admin-access",
                      "bypass-approval"
                    ]
                  }
                },
                "superadmin": {
                  "statements": {
                    "user": [
                      "create",
                      "list",
                      "set-role",
                      "ban",
                      "impersonate",
                      "delete",
                      "set-password",
                      "get",
                      "update",
                      "impersonate-admins"
                    ],
                    "session": [
                      "list",
                      "revoke",
                      "delete"
                    ],
                    "project": [
                      "read",
                      "manage"
                    ],
                    "validation": [
                      "review",
                      "finalize",
                      "report"
                    ],
                    "app": [
                      "admin-access",
                      "bypass-approval"
                    ],
                    "role": [
                      "manage-sensitive"
                    ]
                  }
                }
              },
              "defaultRole": "user",
              "adminRoles": [
                "admin",
                "superadmin"
              ]
            }
          }
        },
        {
          "name": "generic-oauth",
          "config": {
            "id": "generic-oauth",
            "version": "1.6.0",
            "endpoints": {},
            "options": {
              "config": [
                {
                  "providerId": "scheduler",
                  "clientId": "[REDACTED]",
                  "clientSecret": "[REDACTED]",
                  "pkce": true,
                  "discoveryUrl": "http://localhost:3000/.well-known/openid-configuration",
                  "scopes": [
                    "email",
                    "profile",
                    "offline_access",
                    "openid",
                    "schedule:read"
                  ],
                  "redirectURI": "http://localhost:3010/api/auth/callback/scheduler"
                }
              ]
            },
            "$ERROR_CODES": {
              "INVALID_OAUTH_CONFIGURATION": {
                "code": "INVALID_OAUTH_CONFIGURATION",
                "message": "Invalid OAuth configuration"
              },
              "TOKEN_URL_NOT_FOUND": {
                "code": "TOKEN_URL_NOT_FOUND",
                "message": "Invalid OAuth configuration. Token URL not found."
              },
              "PROVIDER_CONFIG_NOT_FOUND": {
                "code": "PROVIDER_CONFIG_NOT_FOUND",
                "message": "No config found for provider"
              },
              "PROVIDER_ID_REQUIRED": {
                "code": "PROVIDER_ID_REQUIRED",
                "message": "Provider ID is required"
              },
              "INVALID_OAUTH_CONFIG": {
                "code": "INVALID_OAUTH_CONFIG",
                "message": "Invalid OAuth configuration."
              },
              "SESSION_REQUIRED": {
                "code": "SESSION_REQUIRED",
                "message": "Session is required"
              },
              "ISSUER_MISMATCH": {
                "code": "ISSUER_MISMATCH",
                "message": "OAuth issuer mismatch. The authorization server issuer does not match the expected value (RFC 9207)."
              },
              "ISSUER_MISSING": {
                "code": "ISSUER_MISSING",
                "message": "OAuth issuer parameter missing. The authorization server did not include the required iss parameter (RFC 9207)."
              }
            }
          }
        },
        {
          "name": "oauth-provider-client",
          "config": {
            "id": "oauth-provider-client",
            "version": "1.6.0",
            "fetchPlugins": [
              {
                "id": "oauth-provider-signin",
                "name": "oauth-provider-signin",
                "description": "Adds the current page query to oauth requests",
                "hooks": {}
              }
            ],
            "$InferServerPlugin": {}
          }
        },
        {
          "name": "tanstack-start-cookies",
          "config": {
            "id": "tanstack-start-cookies",
            "version": "1.6.0",
            "hooks": {
              "after": [
                {}
              ]
            }
          }
        }
      ]
    }
  }
}

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

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth"

const LOCAL_DEV_ORIGINS = [
  'localhost:3000',
  'localhost:3010',
]

export const auth = betterAuth({
  baseURL: {
    allowedHosts: [...LOCAL_DEV_ORIGINS],
    protocol: process.env.NODE_ENV === 'development' ? 'http' : 'https',
  },
});

Additional context

TypeError: Invalid URL
at new URL (node:internal/url:826:25)
at C:/Users/jeanf/source/repos/gcf-paie-app/node_modules/.pnpm/better-auth@1.6.0_@opentele_987cc74542654a08bb9a396b1cdafa8a/node_modules/better-auth/dist/plugins/magic-link/index.mjs:67:25
at processTicksAndRejections (node:internal/process/task_queues:105:5)
at internalHandler (file:///C:/Users/jeanf/source/repos/gcf-paie-app/node_modules/.pnpm/better-call@1.3.5_zod@4.3.6/node_modules/better-call/dist/endpoint.mjs:30:20)
at C:/Users/jeanf/source/repos/gcf-paie-app/node_modules/.pnpm/better-auth@1.6.0_@opentele_987cc74542654a08bb9a396b1cdafa8a/node_modules/better-auth/dist/api/to-auth-endpoints.mjs:73:21
at Object.serverFn (C:/Users/jeanf/source/repos/gcf-paie-app/src/lib/users.api.ts:306:7)
at server (C:/Users/jeanf/source/repos/gcf-paie-app/node_modules/.pnpm/@tanstack+start-client-core@1.167.9/node_modules/@tanstack/start-client-core/src/createServerFn.ts:798:24)
at callNextMiddleware (C:/Users/jeanf/source/repos/gcf-paie-app/node_modules/.pnpm/@tanstack+start-client-core@1.167.9/node_modules/@tanstack/start-client-core/src/createServerFn.ts:289:24)
at Object.assign.__executeServer (C:/Users/jeanf/source/repos/gcf-paie-app/node_modules/.pnpm/@tanstack+start-client-core@1.167.9/node_modules/@tanstack/start-client-core/src/createServerFn.ts:171:28)
at C:/Users/jeanf/source/repos/gcf-paie-app/node_modules/.pnpm/@tanstack+start-server-core_902d47834cfd6b4f9c10ffc33b2f1af5/node_modules/@tanstack/start-server-core/src/server-functions-handler.ts:161:16 {
code: 'ERR_INVALID_URL',
input: ''

Similar to #8559

Perhaps looking every place this is used could correct a lot of these bugs?

52c47517a2/packages/better-auth/src/plugins/magic-link/index.ts (L231):

line 231:
const realBaseURL = new URL(ctx.context.baseURL);

Originally created by @j-fdion on GitHub (Apr 10, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/9105 Originally assigned to: @bytaesu on GitHub. ### Is this suited for github? - [x] Yes, this is suited for github ### Reproduction 1. Configure betterAuth with: ``` import { betterAuth } from "better-auth" const LOCAL_DEV_ORIGINS = [ 'localhost:3000', 'localhost:3010', ] export const auth = betterAuth({ baseURL: { allowedHosts: [...LOCAL_DEV_ORIGINS], protocol: process.env.NODE_ENV === 'development' ? 'http' : 'https', }, }); ``` 2. use magic link: ``` await auth.api.signInMagicLink({ body: { email, callbackURL: '/' }, headers: request.headers, }) ``` ### Current vs. Expected behavior signInMagicLink should not throw ### What version of Better Auth are you using? 1.6.0 ### System info ```bash { "system": { "platform": "win32", "arch": "x64", "version": "Windows 11 Pro", "release": "10.0.26200", "cpuCount": 16, "cpuModel": "AMD Ryzen 7 9800X3D 8-Core Processor ", "totalMemory": "31.06 GB", "freeMemory": "13.24 GB" }, "node": { "version": "v24.6.0", "env": "development" }, "packageManager": { "name": "npm", "version": "11.5.1" }, "frameworks": [ { "name": "react", "version": "^19.2.5" } ], "databases": [ { "name": "pg", "version": "^8.20.0" }, { "name": "drizzle", "version": "^0.45.2" } ], "betterAuth": { "version": "^1.6.2", "config": { "baseURL": { "allowedHosts": [ "localhost:3000", "localhost:3010", "127.0.0.1:3000", "*.railway.app" ], "protocol": "https" }, "advanced": { "cookiePrefix": "gcf-paie", "crossSubDomainCookies": { "enabled": true } }, "account": { "accountLinking": { "allowDifferentEmails": true, "trustedProviders": [ "scheduler" ] } }, "user": { "additionalFields": { "matricule": { "type": "string", "required": false, "nullable": true, "input": false, "returned": true }, "approvedAt": { "type": "date", "required": false, "nullable": true, "input": false, "returned": true } } }, "telemetry": { "enabled": true, "debug": false }, "logger": { "disabled": false, "disableColors": false, "level": "debug" }, "plugins": [ { "name": "magic-link", "config": { "id": "magic-link", "version": "1.6.0", "endpoints": {}, "rateLimit": [ { "window": 60, "max": 5 } ], "options": {} } }, { "name": "admin", "config": { "id": "admin", "version": "1.6.0", "hooks": { "after": [ {} ] }, "endpoints": {}, "$ERROR_CODES": { "FAILED_TO_CREATE_USER": { "code": "FAILED_TO_CREATE_USER", "message": "Failed to create user" }, "USER_ALREADY_EXISTS": { "code": "USER_ALREADY_EXISTS", "message": "User already exists." }, "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": { "code": "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL", "message": "User already exists. Use another email." }, "YOU_CANNOT_BAN_YOURSELF": { "code": "YOU_CANNOT_BAN_YOURSELF", "message": "You cannot ban yourself" }, "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": { "code": "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE", "message": "You are not allowed to change users role" }, "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": { "code": "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS", "message": "You are not allowed to create users" }, "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": { "code": "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS", "message": "You are not allowed to list users" }, "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": { "code": "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS", "message": "You are not allowed to list users sessions" }, "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": { "code": "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS", "message": "You are not allowed to ban users" }, "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": { "code": "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS", "message": "You are not allowed to impersonate users" }, "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": { "code": "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS", "message": "You are not allowed to revoke users sessions" }, "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": { "code": "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS", "message": "You are not allowed to delete users" }, "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": { "code": "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD", "message": "You are not allowed to set users password" }, "BANNED_USER": { "code": "BANNED_USER", "message": "You have been banned from this application" }, "YOU_ARE_NOT_ALLOWED_TO_GET_USER": { "code": "YOU_ARE_NOT_ALLOWED_TO_GET_USER", "message": "You are not allowed to get user" }, "NO_DATA_TO_UPDATE": { "code": "NO_DATA_TO_UPDATE", "message": "No data to update" }, "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": { "code": "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS", "message": "You are not allowed to update users" }, "YOU_CANNOT_REMOVE_YOURSELF": { "code": "YOU_CANNOT_REMOVE_YOURSELF", "message": "You cannot remove yourself" }, "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE": { "code": "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE", "message": "You are not allowed to set a non-existent role value" }, "YOU_CANNOT_IMPERSONATE_ADMINS": { "code": "YOU_CANNOT_IMPERSONATE_ADMINS", "message": "You cannot impersonate admins" }, "INVALID_ROLE_TYPE": { "code": "INVALID_ROLE_TYPE", "message": "Invalid role type" } }, "schema": { "user": { "fields": { "role": { "type": "string", "required": false, "input": false }, "banned": { "type": "boolean", "defaultValue": false, "required": false, "input": false }, "banReason": { "type": "string", "required": false, "input": false }, "banExpires": { "type": "date", "required": false, "input": false } } }, "session": { "fields": { "impersonatedBy": { "type": "string", "required": false } } } }, "options": { "ac": { "statements": { "user": [ "create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update" ], "session": [ "list", "revoke", "delete" ], "project": [ "read", "manage" ], "validation": [ "review", "finalize", "report" ], "app": [ "admin-access", "bypass-approval" ], "role": [ "manage-sensitive" ] } }, "roles": { "user": { "statements": { "project": [ "read" ] } }, "supervisor": { "statements": { "project": [ "read", "manage" ], "validation": [ "review", "report" ] } }, "admin": { "statements": { "user": [ "create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update" ], "session": [ "list", "revoke", "delete" ], "project": [ "read", "manage" ], "validation": [ "review", "finalize", "report" ], "app": [ "admin-access", "bypass-approval" ] } }, "superadmin": { "statements": { "user": [ "create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update", "impersonate-admins" ], "session": [ "list", "revoke", "delete" ], "project": [ "read", "manage" ], "validation": [ "review", "finalize", "report" ], "app": [ "admin-access", "bypass-approval" ], "role": [ "manage-sensitive" ] } } }, "defaultRole": "user", "adminRoles": [ "admin", "superadmin" ] } } }, { "name": "generic-oauth", "config": { "id": "generic-oauth", "version": "1.6.0", "endpoints": {}, "options": { "config": [ { "providerId": "scheduler", "clientId": "[REDACTED]", "clientSecret": "[REDACTED]", "pkce": true, "discoveryUrl": "http://localhost:3000/.well-known/openid-configuration", "scopes": [ "email", "profile", "offline_access", "openid", "schedule:read" ], "redirectURI": "http://localhost:3010/api/auth/callback/scheduler" } ] }, "$ERROR_CODES": { "INVALID_OAUTH_CONFIGURATION": { "code": "INVALID_OAUTH_CONFIGURATION", "message": "Invalid OAuth configuration" }, "TOKEN_URL_NOT_FOUND": { "code": "TOKEN_URL_NOT_FOUND", "message": "Invalid OAuth configuration. Token URL not found." }, "PROVIDER_CONFIG_NOT_FOUND": { "code": "PROVIDER_CONFIG_NOT_FOUND", "message": "No config found for provider" }, "PROVIDER_ID_REQUIRED": { "code": "PROVIDER_ID_REQUIRED", "message": "Provider ID is required" }, "INVALID_OAUTH_CONFIG": { "code": "INVALID_OAUTH_CONFIG", "message": "Invalid OAuth configuration." }, "SESSION_REQUIRED": { "code": "SESSION_REQUIRED", "message": "Session is required" }, "ISSUER_MISMATCH": { "code": "ISSUER_MISMATCH", "message": "OAuth issuer mismatch. The authorization server issuer does not match the expected value (RFC 9207)." }, "ISSUER_MISSING": { "code": "ISSUER_MISSING", "message": "OAuth issuer parameter missing. The authorization server did not include the required iss parameter (RFC 9207)." } } } }, { "name": "oauth-provider-client", "config": { "id": "oauth-provider-client", "version": "1.6.0", "fetchPlugins": [ { "id": "oauth-provider-signin", "name": "oauth-provider-signin", "description": "Adds the current page query to oauth requests", "hooks": {} } ], "$InferServerPlugin": {} } }, { "name": "tanstack-start-cookies", "config": { "id": "tanstack-start-cookies", "version": "1.6.0", "hooks": { "after": [ {} ] } } } ] } } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" const LOCAL_DEV_ORIGINS = [ 'localhost:3000', 'localhost:3010', ] export const auth = betterAuth({ baseURL: { allowedHosts: [...LOCAL_DEV_ORIGINS], protocol: process.env.NODE_ENV === 'development' ? 'http' : 'https', }, }); ``` ### Additional context TypeError: Invalid URL at new URL (node:internal/url:826:25) at C:/Users/jeanf/source/repos/gcf-paie-app/node_modules/.pnpm/better-auth@1.6.0_@opentele_987cc74542654a08bb9a396b1cdafa8a/node_modules/better-auth/dist/plugins/magic-link/index.mjs:67:25 at processTicksAndRejections (node:internal/process/task_queues:105:5) at internalHandler (file:///C:/Users/jeanf/source/repos/gcf-paie-app/node_modules/.pnpm/better-call@1.3.5_zod@4.3.6/node_modules/better-call/dist/endpoint.mjs:30:20) at C:/Users/jeanf/source/repos/gcf-paie-app/node_modules/.pnpm/better-auth@1.6.0_@opentele_987cc74542654a08bb9a396b1cdafa8a/node_modules/better-auth/dist/api/to-auth-endpoints.mjs:73:21 at Object.serverFn (C:/Users/jeanf/source/repos/gcf-paie-app/src/lib/users.api.ts:306:7) at server (C:/Users/jeanf/source/repos/gcf-paie-app/node_modules/.pnpm/@tanstack+start-client-core@1.167.9/node_modules/@tanstack/start-client-core/src/createServerFn.ts:798:24) at callNextMiddleware (C:/Users/jeanf/source/repos/gcf-paie-app/node_modules/.pnpm/@tanstack+start-client-core@1.167.9/node_modules/@tanstack/start-client-core/src/createServerFn.ts:289:24) at Object.assign.__executeServer (C:/Users/jeanf/source/repos/gcf-paie-app/node_modules/.pnpm/@tanstack+start-client-core@1.167.9/node_modules/@tanstack/start-client-core/src/createServerFn.ts:171:28) at C:/Users/jeanf/source/repos/gcf-paie-app/node_modules/.pnpm/@tanstack+start-server-core_902d47834cfd6b4f9c10ffc33b2f1af5/node_modules/@tanstack/start-server-core/src/server-functions-handler.ts:161:16 { code: 'ERR_INVALID_URL', input: '' Similar to #8559 Perhaps looking every place this is used could correct a lot of these bugs? https://github.com/better-auth/better-auth/blob/52c47517a21600d40a3e82c427409083b4a0a9ec/packages/better-auth/src/plugins/magic-link/index.ts#L231: line 231: ` const realBaseURL = new URL(ctx.context.baseURL); `
GiteaMirror added the credentials label 2026-04-15 19:16:23 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Apr 10, 2026):

This is the same class of bug as #8559 — the magic-link plugin does new URL(ctx.context.baseURL) at line 231 of magic-link/index.ts, and with the object-based baseURL config, ctx.context.baseURL is an empty string at that point, causing TypeError: Invalid URL.

The oauth-provider plugin got a specific fix for this in PR #8649, but that fix doesn't cover the magic-link plugin. There is also PR #8455 which aims to fix this framework-wide by using the fallback URL as the initial baseURL, but it's still open.

Workaround: Add a fallback to your dynamic baseURL config:

baseURL: {
  allowedHosts: [...LOCAL_DEV_ORIGINS],
  protocol: process.env.NODE_ENV === 'development' ? 'http' : 'https',
  fallback: process.env.NODE_ENV === 'development'
    ? 'http://localhost:3000'
    : 'https://your-production-url.com',
}

This should give the magic-link plugin a valid URL to work with instead of an empty string.

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:4225091559 --> @dosubot[bot] commented on GitHub (Apr 10, 2026): <!-- Answer --> This is the same class of bug as #8559 — the magic-link plugin does `new URL(ctx.context.baseURL)` at [line 231 of `magic-link/index.ts`](https://github.com/better-auth/better-auth/blob/52c47517a21600d40a3e82c427409083b4a0a9ec/packages/better-auth/src/plugins/magic-link/index.ts#L231), and with the object-based `baseURL` config, `ctx.context.baseURL` is an empty string at that point, causing `TypeError: Invalid URL`. The oauth-provider plugin got a specific fix for this in [PR #8649](https://github.com/better-auth/better-auth/pull/8649), but that fix doesn't cover the magic-link plugin. There is also [PR #8455](https://github.com/better-auth/better-auth/pull/8455) which aims to fix this framework-wide by using the `fallback` URL as the initial baseURL, but it's still open. **Workaround:** Add a `fallback` to your dynamic baseURL config: ```typescript baseURL: { allowedHosts: [...LOCAL_DEV_ORIGINS], protocol: process.env.NODE_ENV === 'development' ? 'http' : 'https', fallback: process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : 'https://your-production-url.com', } ``` This should give the magic-link plugin a valid URL to work with instead of an empty string. <!-- 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?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-great_response#message_id=f924ebe3-751a-44c5-87d3-51c126b42853&feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-irrelevant_answer#message_id=f924ebe3-751a-44c5-87d3-51c126b42853&feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-incorrect_sources#message_id=f924ebe3-751a-44c5-87d3-51c126b42853&feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-too_verbose#message_id=f924ebe3-751a-44c5-87d3-51c126b42853&feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-hallucination#message_id=f924ebe3-751a-44c5-87d3-51c126b42853&feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-bug_report#message_id=f924ebe3-751a-44c5-87d3-51c126b42853&feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-other#message_id=f924ebe3-751a-44c5-87d3-51c126b42853&feedback_type=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%20the%20same%20class%20of%20bug%20as%20%238559%20%E2%80%94%20the%20magic-link%20plugin%20does%20%60new%20URL%28ctx.context.baseURL%29%60%20at%20%5Bline%20231%20of%20%60magic-link/index.ts%60%5D%28https%3A//github.com/better-auth/better-auth/blob/52c47517a21600d40a3e82c427409083b4a0a9ec/packages/better-auth/src/plugins/magic-link/index.ts%23L231%29%2C%20and%20with%20the%20object-based%20%60baseURL%60%20config%2C%20%60ctx.context.baseURL%60%20is%20an%20empty%20string%20at%20that%20point%2C%20causing%20%60TypeError%3A%20Invalid%20URL%60.%0A%0AThe%20oauth-provider%20plugin%20got%20a%20specific%20fix%20for%20this%20in%20%5BPR%20%238649%5D%28https%3A//github.com/better-auth/better-auth/pull/8649%29%2C%20but%20that%20fix%20doesn%27t%20cover%20the%20magic-link%20plugin.%20There%20is%20also%20%5BPR%20%238455%5D%28https%3A//github.com/better-auth/better-auth/pull/8455%29%20which%20aims%20to%20fix%20this%20framework-wide%20by%20using%20the%20%60fallback%60%20URL%20as%20the%20initial%20baseURL%2C%20but%20it%27s%20still%20open.%0A%0A%2A%2AWorkaround%3A%2A%2A%20Add%20a%20%60fallback%60%20to%20your%20dynamic%20baseURL%20config%3A%0A%0A%60%60%60typescript%0AbaseURL%3A%20%7B%0A%20%20allowedHosts%3A%20%5B...LOCAL_DEV_ORIGINS%5D%2C%0A%20%20protocol%3A%20process.env.NODE_ENV%20%3D%3D%3D%20%27development%27%20%3F%20%27http%27%20%3A%20%27https%27%2C%0A%20%20fallback%3A%20process.env.NODE_ENV%20%3D%3D%3D%20%27development%27%0A%20%20%20%20%3F%20%27http%3A//localhost%3A3000%27%0A%20%20%20%20%3A%20%27https%3A//your-production-url.com%27%2C%0A%7D%0A%60%60%60%0A%0AThis%20should%20give%20the%20magic-link%20plugin%20a%20valid%20URL%20to%20work%20with%20instead%20of%20an%20empty%20string.)&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/9105)
Author
Owner

@j-fdion commented on GitHub (Apr 10, 2026):

Your work around does not work 🥸

<!-- gh-comment-id:4225130336 --> @j-fdion commented on GitHub (Apr 10, 2026): Your work around does not work 🥸
Author
Owner

@dosubot[bot] commented on GitHub (Apr 10, 2026):

You're right — the fallback doesn't actually help here. The root cause is that the fallback property is only consulted during per-request baseURL resolution, but when you call auth.api.signInMagicLink() server-side, it bypasses the HTTP request handler, so the per-request resolution never runs. The initial ctx.context.baseURL remains "", and the magic-link plugin does new URL("") which throws [1] [2].

PR #8455 would fix this by using the fallback to populate the initial baseURL, but it's still open.

What should work: Set the BETTER_AUTH_URL environment variable, which gets picked up during initial context creation regardless of your dynamic config [3]:

# .env.development
BETTER_AUTH_URL=http://localhost:3000

Or use a static string baseURL instead of the object format:

baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3000',

Either of these ensures the magic-link plugin gets a valid URL even for server-side API calls.

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:4225178561 --> @dosubot[bot] commented on GitHub (Apr 10, 2026): <!-- Answer --> You're right — the `fallback` doesn't actually help here. The root cause is that the `fallback` property is only consulted during **per-request** baseURL resolution, but when you call `auth.api.signInMagicLink()` server-side, it bypasses the HTTP request handler, so the per-request resolution never runs. The initial `ctx.context.baseURL` remains `""`, and the magic-link plugin does `new URL("")` which throws [[1]](https://github.com/better-auth/better-auth/blob/52c47517a21600d40a3e82c427409083b4a0a9ec/packages/better-auth/src/plugins/magic-link/index.ts#L231) [[2]](https://github.com/better-auth/better-auth/blob/52c47517a21600d40a3e82c427409083b4a0a9ec/packages/better-auth/src/context/create-context.ts#L256). [PR #8455](https://github.com/better-auth/better-auth/pull/8455) would fix this by using the fallback to populate the initial baseURL, but it's still open. **What should work:** Set the `BETTER_AUTH_URL` environment variable, which gets picked up during initial context creation regardless of your dynamic config [[3]](https://github.com/better-auth/better-auth/blob/52c47517a21600d40a3e82c427409083b4a0a9ec/packages/better-auth/src/utils/url.ts#L120-L126): ```bash # .env.development BETTER_AUTH_URL=http://localhost:3000 ``` Or use a static string `baseURL` instead of the object format: ```typescript baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3000', ``` Either of these ensures the magic-link plugin gets a valid URL even for server-side API calls. <!-- 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?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-great_response#message_id=d1f0544a-13a9-4db4-a88b-e8cf32ef045f&feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-irrelevant_answer#message_id=d1f0544a-13a9-4db4-a88b-e8cf32ef045f&feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-incorrect_sources#message_id=d1f0544a-13a9-4db4-a88b-e8cf32ef045f&feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-too_verbose#message_id=d1f0544a-13a9-4db4-a88b-e8cf32ef045f&feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-hallucination#message_id=d1f0544a-13a9-4db4-a88b-e8cf32ef045f&feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-bug_report#message_id=d1f0544a-13a9-4db4-a88b-e8cf32ef045f&feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-other#message_id=d1f0544a-13a9-4db4-a88b-e8cf32ef045f&feedback_type=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=You%27re%20right%20%E2%80%94%20the%20%60fallback%60%20doesn%27t%20actually%20help%20here.%20The%20root%20cause%20is%20that%20the%20%60fallback%60%20property%20is%20only%20consulted%20during%20%2A%2Aper-request%2A%2A%20baseURL%20resolution%2C%20but%20when%20you%20call%20%60auth.api.signInMagicLink%28%29%60%20server-side%2C%20it%20bypasses%20the%20HTTP%20request%20handler%2C%20so%20the%20per-request%20resolution%20never%20runs.%20The%20initial%20%60ctx.context.baseURL%60%20remains%20%60%22%22%60%2C%20and%20the%20magic-link%20plugin%20does%20%60new%20URL%28%22%22%29%60%20which%20throws%20%5B%5B1%5D%5D%28https%3A//github.com/better-auth/better-auth/blob/52c47517a21600d40a3e82c427409083b4a0a9ec/packages/better-auth/src/plugins/magic-link/index.ts%23L231%29%20%5B%5B2%5D%5D%28https%3A//github.com/better-auth/better-auth/blob/52c47517a21600d40a3e82c427409083b4a0a9ec/packages/better-auth/src/context/create-context.ts%23L256%29.%0A%0A%5BPR%20%238455%5D%28https%3A//github.com/better-auth/better-auth/pull/8455%29%20would%20fix%20this%20by%20using%20the%20fallback%20to%20populate%20the%20initial%20baseURL%2C%20but%20it%27s%20still%20open.%0A%0A%2A%2AWhat%20should%20work%3A%2A%2A%20Set%20the%20%60BETTER_AUTH_URL%60%20environment%20variable%2C%20which%20gets%20picked%20up%20during%20initial%20context%20creation%20regardless%20of%20your%20dynamic%20config%20%5B%5B3%5D%5D%28https%3A//github.com/better-auth/better-auth/blob/52c47517a21600d40a3e82c427409083b4a0a9ec/packages/better-auth/src/utils/url.ts%23L120-L126%29%3A%0A%0A%60%60%60bash%0A%23%20.env.development%0ABETTER_AUTH_URL%3Dhttp%3A//localhost%3A3000%0A%60%60%60%0A%0AOr%20use%20a%20static%20string%20%60baseURL%60%20instead%20of%20the%20object%20format%3A%0A%0A%60%60%60typescript%0AbaseURL%3A%20process.env.BETTER_AUTH_URL%20%7C%7C%20%27http%3A//localhost%3A3000%27%2C%0A%60%60%60%0A%0AEither%20of%20these%20ensures%20the%20magic-link%20plugin%20gets%20a%20valid%20URL%20even%20for%20server-side%20API%20calls.)&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/9105)
Author
Owner

@humemm commented on GitHub (Apr 11, 2026):

This also affects oauthProviderAuthServerMetadata and oauthProviderOpenIdConfigMetadata in @better-auth/oauth-provider. The metadata endpoints use ctx.context.baseURL directly for the issuer field, resulting in empty issuer in OpenID configuration when dynamic baseURL is configured:

{"issuer":"", "authorization_endpoint":"/oauth2/authorize", ...}

Root cause: packages/oauth-provider/src/metadata.ts lines 151-161 receive _request but never pass it to the internal API call, so dynamic baseURL cannot be resolved.

<!-- gh-comment-id:4230387672 --> @humemm commented on GitHub (Apr 11, 2026): This also affects `oauthProviderAuthServerMetadata` and `oauthProviderOpenIdConfigMetadata` in `@better-auth/oauth-provider`. The metadata endpoints use `ctx.context.baseURL` directly for the `issuer` field, resulting in empty issuer in OpenID configuration when dynamic baseURL is configured: ```json {"issuer":"", "authorization_endpoint":"/oauth2/authorize", ...} ``` Root cause: `packages/oauth-provider/src/metadata.ts` lines 151-161 receive `_request` but never pass it to the internal API call, so dynamic baseURL cannot be resolved.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#19907