Expo SDK54 results in "TypeError: Can't modify immutable headers" #2201

Closed
opened 2026-03-13 09:34:04 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @jinsley8 on GitHub (Oct 25, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Upgrade from Expo SDK 53 to SDK 54.
  2. I use a hono.js server with better-auth expo plugin
  3. After upgrading to SDK 54 I see the following error when expo-secure-store tries to load at app launch:

Unhandled error: TypeError: Can't modify immutable headers.

  1. Adding the following option to the expo plugin fixes the error: expo({ disableOriginOverride: true })

Current vs. Expected behavior

Before upgrading to Expo SDK v54, I did not encounter this error. I had to use expo({ disableOriginOverride: true }) to fix it. Is this the correct solution?

What version of Better Auth are you using?

1.3.27 - also tried upgrading to 1.3.31

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.6.0: Mon Aug 11 21:16:21 PDT 2025; root:xnu-11417.140.69.701.11~1/RELEASE_ARM64_T6000",
    "release": "24.6.0",
    "cpuCount": 10,
    "cpuModel": "Apple M1 Pro",
    "totalMemory": "32.00 GB",
    "freeMemory": "0.14 GB"
  },
  "node": {
    "version": "v22.16.0",
    "env": "development"
  },
  "packageManager": {
    "name": "pnpm",
    "version": "10.15.1"
  },
  "frameworks": null,
  "databases": [
    {
      "name": "drizzle",
      "version": "catalog:"
    }
  ],
  "betterAuth": {
    "version": "1.3.27",
    "config": {
      "trustedOrigins": [
        "http://localhost:3001"
      ],
      "baseURL": "http://localhost:8787",
      "secret": "[REDACTED]",
      "user": {
        "additionalFields": {
          "firstName": {
            "type": "string",
            "required": false
          },
          "lastName": {
            "type": "string",
            "required": false
          },
          "preferredName": {
            "type": "string",
            "required": false
          }
        }
      },
      "emailAndPassword": {
        "enabled": true,
        "requireEmailVerification": true
      },
      "socialProviders": {
        "google": {
          "clientId": "",
          "clientSecret": ""
        }
      },
      "plugins": [
        {
          "name": "expo",
          "config": {
            "id": "expo",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {}
          }
        },
        {
          "name": "username",
          "config": {
            "id": "username",
            "endpoints": {},
            "schema": {
              "user": {
                "fields": {
                  "username": {
                    "type": "string",
                    "required": false,
                    "sortable": true,
                    "unique": true,
                    "returned": true,
                    "transform": {}
                  },
                  "displayUsername": {
                    "type": "string",
                    "required": false,
                    "transform": {}
                  }
                }
              }
            },
            "hooks": {
              "before": [
                {},
                {}
              ]
            },
            "$ERROR_CODES": {
              "INVALID_USERNAME_OR_PASSWORD": "[REDACTED]",
              "EMAIL_NOT_VERIFIED": "Email not verified",
              "UNEXPECTED_ERROR": "Unexpected error",
              "USERNAME_IS_ALREADY_TAKEN": "Username is already taken. Please try another.",
              "USERNAME_TOO_SHORT": "Username is too short",
              "USERNAME_TOO_LONG": "Username is too long",
              "INVALID_USERNAME": "Username is invalid",
              "INVALID_DISPLAY_USERNAME": "Display username is invalid"
            }
          }
        },
        {
          "name": "admin",
          "config": {
            "id": "admin",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {},
            "$ERROR_CODES": {
              "FAILED_TO_CREATE_USER": "Failed to create user",
              "USER_ALREADY_EXISTS": "User already exists.",
              "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "User already exists. Use another email.",
              "YOU_CANNOT_BAN_YOURSELF": "You cannot ban yourself",
              "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": "You are not allowed to create users",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": "You are not allowed to list users",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": "You are not allowed to list users sessions",
              "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": "You are not allowed to ban users",
              "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": "You are not allowed to impersonate users",
              "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": "You are not allowed to revoke users sessions",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": "You are not allowed to delete users",
              "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": "[REDACTED]",
              "BANNED_USER": "You have been banned from this application",
              "YOU_ARE_NOT_ALLOWED_TO_GET_USER": "You are not allowed to get user",
              "NO_DATA_TO_UPDATE": "No data to update",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": "You are not allowed to update users",
              "YOU_CANNOT_REMOVE_YOURSELF": "You cannot remove yourself",
              "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE": "You are not allowed to set a non-existent role value"
            },
            "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": {
              "defaultRole": "user",
              "adminRoles": [
                "superadmin",
                "admin",
                "game-ops",
                "support"
              ],
              "adminUserIds": []
            }
          }
        },
        {
          "name": "email-otp",
          "config": {
            "id": "email-otp",
            "endpoints": {},
            "hooks": {
              "after": [
                {}
              ]
            },
            "$ERROR_CODES": {
              "OTP_EXPIRED": "otp expired",
              "INVALID_OTP": "Invalid OTP",
              "INVALID_EMAIL": "Invalid email",
              "USER_NOT_FOUND": "User not found",
              "TOO_MANY_ATTEMPTS": "Too many attempts"
            },
            "rateLimit": [
              {
                "window": 60,
                "max": 3
              },
              {
                "window": 60,
                "max": 3
              },
              {
                "window": 60,
                "max": 3
              },
              {
                "window": 60,
                "max": 3
              }
            ]
          }
        },
        {
          "name": "last-login-method",
          "config": {
            "id": "last-login-method",
            "hooks": {
              "after": [
                {}
              ]
            },
            "schema": {
              "user": {
                "fields": {
                  "lastLoginMethod": {
                    "type": "string",
                    "input": false,
                    "required": false,
                    "fieldName": "lastLoginMethod"
                  }
                }
              }
            }
          }
        },
        {
          "name": "stripe",
          "config": {
            "id": "stripe",
            "endpoints": {},
            "schema": {
              "user": {
                "fields": {
                  "stripeCustomerId": {
                    "type": "string",
                    "required": false
                  }
                }
              }
            }
          }
        }
      ]
    }
  }
}

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

Client, Backend

Auth config (if applicable)

export function createMobileAuth(
  config: MobileAuthConfig,
): ReturnType<typeof betterAuth> {
  // Get username validator - enhanced if requested and available, otherwise basic
  const usernameValidator = createEnhancedUsernameValidator();

  // Create Stripe client if credentials are provided
  const stripeClient = config.stripeSecretKey
    ? new Stripe(config.stripeSecretKey, {
      apiVersion: (process.env.STRIPE_API_VERSION ||
        "2025-09-30.clover") as Stripe.LatestApiVersion,
    })
    : undefined;

  return betterAuth({
    appName: "App Name",
    baseURL: config.baseURL,
    secret: config.secret,
    database: drizzleAdapter(config.database, {
      provider: "pg",
      schema,
      usePlural: false,
      debugLogs: true,
    }),
    trustedOrigins: config.trustedOrigins,
    // Configure reset token expiration window when provided
    resetPasswordTokenExpiresIn: config.resetPasswordTokenExpiresIn,
    // Error handling configuration
    onAPIError: config.errorURL
      ? {
        errorURL: config.errorURL,
      }
      : undefined,
    user: {
      additionalFields: {
        firstName: { type: "string", required: false },
        lastName: { type: "string", required: false },
        preferredName: { type: "string", required: false },
        // Fields from profiles table added via customSession
        onboardingComplete: { type: "boolean", required: false, input: false },
        activeRole: { type: "string", required: false, input: false },
      },
    },
    emailAndPassword: {
      enabled: true,
      // Wire in password reset email + callback when provided by the host app
      sendResetPassword: config.sendResetPassword,
      onPasswordReset: config.onPasswordReset,
      sendVerificationEmail: config.sendVerificationEmail,
    },
    socialProviders: config.googleClientId && config.googleClientSecret
      ? {
        google: {
          prompt: "select_account",
          clientId: config.googleClientId,
          clientSecret: config.googleClientSecret,
          scope: ["email", "profile"],
          // For mobile, we need to use a static redirect URI that matches the deep link
          // The Expo plugin will handle the actual callback processing
          // redirectURI: `${config.baseURL}/api/auth/callback/google`,
          // No domain restrictions for mobile users
          mapProfileToUser: (profile) => {
            const email = profile.email?.toLowerCase();

            // Check the image URL before assigning it
            const customImage = isDefaultGoogleAvatarUrl(profile.picture)
              ? undefined // It's a default avatar, so we store null.
              : profile.picture; // It's a custom photo, so we keep it.

            return {
              email: email,
              name: profile.name,
              firstName: profile.given_name,
              lastName: profile.family_name,
              image: customImage,
              emailVerified: profile.email_verified || false,
            };
          },
        },
      }
      : undefined,
    session: {
      expiresIn: 60 * 60 * 24 * 30, // 30 days (longer for mobile)
      updateAge: 60 * 60 * 24, // 1 day
    },
    plugins: [
      expo({ disableOriginOverride: true }),
      username({
        minUsernameLength: 3,
        maxUsernameLength: 20,
        usernameValidator,
      }),
      ...(stripeClient && config.stripeWebhookSecret
        ? [
          stripe({
            stripeClient,
            stripeWebhookSecret: config.stripeWebhookSecret,
            createCustomerOnSignUp: true,
          }),
        ]
        : []),
      lastLoginMethod({
        storeInDatabase: true,
      }),
    ] as Parameters<typeof betterAuth>[0]["plugins"],
    logger: {
      disabled: false,
      level: "error",
      log: (level, message) => {
        console.log(message, `level : ${level}`);
      },
    },
    advanced: {
      database: {
        generateId: false,
        useNumberId: false,
      },
    },
  });
}

Additional context

No response

Originally created by @jinsley8 on GitHub (Oct 25, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Upgrade from Expo SDK 53 to SDK 54. 2. I use a hono.js server with better-auth expo plugin 3. After upgrading to SDK 54 I see the following error when expo-secure-store tries to load at app launch: `Unhandled error: TypeError: Can't modify immutable headers.` 4. Adding the following option to the expo plugin fixes the error: `expo({ disableOriginOverride: true })` ### Current vs. Expected behavior Before upgrading to Expo SDK v54, I did not encounter this error. I had to use `expo({ disableOriginOverride: true })` to fix it. Is this the correct solution? ### What version of Better Auth are you using? 1.3.27 - also tried upgrading to 1.3.31 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.6.0: Mon Aug 11 21:16:21 PDT 2025; root:xnu-11417.140.69.701.11~1/RELEASE_ARM64_T6000", "release": "24.6.0", "cpuCount": 10, "cpuModel": "Apple M1 Pro", "totalMemory": "32.00 GB", "freeMemory": "0.14 GB" }, "node": { "version": "v22.16.0", "env": "development" }, "packageManager": { "name": "pnpm", "version": "10.15.1" }, "frameworks": null, "databases": [ { "name": "drizzle", "version": "catalog:" } ], "betterAuth": { "version": "1.3.27", "config": { "trustedOrigins": [ "http://localhost:3001" ], "baseURL": "http://localhost:8787", "secret": "[REDACTED]", "user": { "additionalFields": { "firstName": { "type": "string", "required": false }, "lastName": { "type": "string", "required": false }, "preferredName": { "type": "string", "required": false } } }, "emailAndPassword": { "enabled": true, "requireEmailVerification": true }, "socialProviders": { "google": { "clientId": "", "clientSecret": "" } }, "plugins": [ { "name": "expo", "config": { "id": "expo", "hooks": { "after": [ {} ] }, "endpoints": {} } }, { "name": "username", "config": { "id": "username", "endpoints": {}, "schema": { "user": { "fields": { "username": { "type": "string", "required": false, "sortable": true, "unique": true, "returned": true, "transform": {} }, "displayUsername": { "type": "string", "required": false, "transform": {} } } } }, "hooks": { "before": [ {}, {} ] }, "$ERROR_CODES": { "INVALID_USERNAME_OR_PASSWORD": "[REDACTED]", "EMAIL_NOT_VERIFIED": "Email not verified", "UNEXPECTED_ERROR": "Unexpected error", "USERNAME_IS_ALREADY_TAKEN": "Username is already taken. Please try another.", "USERNAME_TOO_SHORT": "Username is too short", "USERNAME_TOO_LONG": "Username is too long", "INVALID_USERNAME": "Username is invalid", "INVALID_DISPLAY_USERNAME": "Display username is invalid" } } }, { "name": "admin", "config": { "id": "admin", "hooks": { "after": [ {} ] }, "endpoints": {}, "$ERROR_CODES": { "FAILED_TO_CREATE_USER": "Failed to create user", "USER_ALREADY_EXISTS": "User already exists.", "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "User already exists. Use another email.", "YOU_CANNOT_BAN_YOURSELF": "You cannot ban yourself", "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role", "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": "You are not allowed to create users", "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": "You are not allowed to list users", "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": "You are not allowed to list users sessions", "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": "You are not allowed to ban users", "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": "You are not allowed to impersonate users", "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": "You are not allowed to revoke users sessions", "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": "You are not allowed to delete users", "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": "[REDACTED]", "BANNED_USER": "You have been banned from this application", "YOU_ARE_NOT_ALLOWED_TO_GET_USER": "You are not allowed to get user", "NO_DATA_TO_UPDATE": "No data to update", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": "You are not allowed to update users", "YOU_CANNOT_REMOVE_YOURSELF": "You cannot remove yourself", "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE": "You are not allowed to set a non-existent role value" }, "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": { "defaultRole": "user", "adminRoles": [ "superadmin", "admin", "game-ops", "support" ], "adminUserIds": [] } } }, { "name": "email-otp", "config": { "id": "email-otp", "endpoints": {}, "hooks": { "after": [ {} ] }, "$ERROR_CODES": { "OTP_EXPIRED": "otp expired", "INVALID_OTP": "Invalid OTP", "INVALID_EMAIL": "Invalid email", "USER_NOT_FOUND": "User not found", "TOO_MANY_ATTEMPTS": "Too many attempts" }, "rateLimit": [ { "window": 60, "max": 3 }, { "window": 60, "max": 3 }, { "window": 60, "max": 3 }, { "window": 60, "max": 3 } ] } }, { "name": "last-login-method", "config": { "id": "last-login-method", "hooks": { "after": [ {} ] }, "schema": { "user": { "fields": { "lastLoginMethod": { "type": "string", "input": false, "required": false, "fieldName": "lastLoginMethod" } } } } } }, { "name": "stripe", "config": { "id": "stripe", "endpoints": {}, "schema": { "user": { "fields": { "stripeCustomerId": { "type": "string", "required": false } } } } } } ] } } } ``` ### Which area(s) are affected? (Select all that apply) Client, Backend ### Auth config (if applicable) ```typescript export function createMobileAuth( config: MobileAuthConfig, ): ReturnType<typeof betterAuth> { // Get username validator - enhanced if requested and available, otherwise basic const usernameValidator = createEnhancedUsernameValidator(); // Create Stripe client if credentials are provided const stripeClient = config.stripeSecretKey ? new Stripe(config.stripeSecretKey, { apiVersion: (process.env.STRIPE_API_VERSION || "2025-09-30.clover") as Stripe.LatestApiVersion, }) : undefined; return betterAuth({ appName: "App Name", baseURL: config.baseURL, secret: config.secret, database: drizzleAdapter(config.database, { provider: "pg", schema, usePlural: false, debugLogs: true, }), trustedOrigins: config.trustedOrigins, // Configure reset token expiration window when provided resetPasswordTokenExpiresIn: config.resetPasswordTokenExpiresIn, // Error handling configuration onAPIError: config.errorURL ? { errorURL: config.errorURL, } : undefined, user: { additionalFields: { firstName: { type: "string", required: false }, lastName: { type: "string", required: false }, preferredName: { type: "string", required: false }, // Fields from profiles table added via customSession onboardingComplete: { type: "boolean", required: false, input: false }, activeRole: { type: "string", required: false, input: false }, }, }, emailAndPassword: { enabled: true, // Wire in password reset email + callback when provided by the host app sendResetPassword: config.sendResetPassword, onPasswordReset: config.onPasswordReset, sendVerificationEmail: config.sendVerificationEmail, }, socialProviders: config.googleClientId && config.googleClientSecret ? { google: { prompt: "select_account", clientId: config.googleClientId, clientSecret: config.googleClientSecret, scope: ["email", "profile"], // For mobile, we need to use a static redirect URI that matches the deep link // The Expo plugin will handle the actual callback processing // redirectURI: `${config.baseURL}/api/auth/callback/google`, // No domain restrictions for mobile users mapProfileToUser: (profile) => { const email = profile.email?.toLowerCase(); // Check the image URL before assigning it const customImage = isDefaultGoogleAvatarUrl(profile.picture) ? undefined // It's a default avatar, so we store null. : profile.picture; // It's a custom photo, so we keep it. return { email: email, name: profile.name, firstName: profile.given_name, lastName: profile.family_name, image: customImage, emailVerified: profile.email_verified || false, }; }, }, } : undefined, session: { expiresIn: 60 * 60 * 24 * 30, // 30 days (longer for mobile) updateAge: 60 * 60 * 24, // 1 day }, plugins: [ expo({ disableOriginOverride: true }), username({ minUsernameLength: 3, maxUsernameLength: 20, usernameValidator, }), ...(stripeClient && config.stripeWebhookSecret ? [ stripe({ stripeClient, stripeWebhookSecret: config.stripeWebhookSecret, createCustomerOnSignUp: true, }), ] : []), lastLoginMethod({ storeInDatabase: true, }), ] as Parameters<typeof betterAuth>[0]["plugins"], logger: { disabled: false, level: "error", log: (level, message) => { console.log(message, `level : ${level}`); }, }, advanced: { database: { generateId: false, useNumberId: false, }, }, }); } ``` ### Additional context _No response_
GiteaMirror added the expo label 2026-03-13 09:34:04 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Oct 25, 2025):

Yes, using expo({ disableOriginOverride: true }) is the correct solution for the "TypeError: Can't modify immutable headers" error after upgrading to Expo SDK 54. This error happens because Expo SDK 54 makes request headers immutable, and the better-auth expo plugin previously tried to override the origin header for certain requests. Setting disableOriginOverride: true disables this behavior, preventing the error and is recommended for Expo/mobile environments where header modification is not supported or needed [plugin code].

This workaround is widely used and there are no known negative side effects for typical Expo mobile flows. If you need stricter CSRF protection or custom origin handling, review your trustedOrigins and related config, but for most cases, this flag resolves the issue cleanly [issue discussion].

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 (Oct 25, 2025): <!-- Answer --> Yes, using `expo({ disableOriginOverride: true })` is the correct solution for the "TypeError: Can't modify immutable headers" error after upgrading to Expo SDK 54. This error happens because Expo SDK 54 makes request headers immutable, and the better-auth expo plugin previously tried to override the `origin` header for certain requests. Setting `disableOriginOverride: true` disables this behavior, preventing the error and is recommended for Expo/mobile environments where header modification is not supported or needed [[plugin code](https://github.com/better-auth/better-auth/blob/01e76e7f94ca99bca5d843928efb5eeb5f55fad5/packages/expo/src/index.ts)]. This workaround is widely used and there are no known negative side effects for typical Expo mobile flows. If you need stricter CSRF protection or custom origin handling, review your trustedOrigins and related config, but for most cases, this flag resolves the issue cleanly [[issue discussion](https://github.com/better-auth/better-auth/issues/2058)]. <!-- 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/86effd6a-45c1-4eca-96a6-d128ce201bdc?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/86effd6a-45c1-4eca-96a6-d128ce201bdc?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/86effd6a-45c1-4eca-96a6-d128ce201bdc?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/86effd6a-45c1-4eca-96a6-d128ce201bdc?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/86effd6a-45c1-4eca-96a6-d128ce201bdc?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/86effd6a-45c1-4eca-96a6-d128ce201bdc?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/86effd6a-45c1-4eca-96a6-d128ce201bdc?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/5568)
Author
Owner

@jinsley8 commented on GitHub (Oct 26, 2025):

disableOriginOverride: true fixes this but is not documented anywhere #5545

@jinsley8 commented on GitHub (Oct 26, 2025): `disableOriginOverride: true` fixes this but is not documented anywhere #5545
Author
Owner

@k-yang commented on GitHub (Dec 5, 2025):

Unfortunately, setting disableOriginOverride to true completely wiped the origin header (at least in my development environment).

I'm running a HonoJS backend with the cors plugin, so I think that's causing my /api/auth routes to 403 FORBIDDEN when I run the expo plugin with this setting.

Instead, I wrote a middleware function to manually replace the incoming request's headers with the expo-origin header sent by the client.

app.use("/api/auth/*", async (c, next) => {
  const ExpoOrigin = c.req.header("expo-origin");
  if (ExpoOrigin) {
    const originalRequest = c.req.raw;

    // Create new headers
    const newHeaders = new Headers(originalRequest.headers);
    newHeaders.set("origin", ExpoOrigin);

    // Create a new Request with updated headers
    const newRequest = new Request(originalRequest, {
      headers: newHeaders,
    });

    // Replace the request in context
    c.req.raw = newRequest;
  }
  await next();
});

With this, you don't even need to set the disableOriginOverride.

@k-yang commented on GitHub (Dec 5, 2025): Unfortunately, setting `disableOriginOverride` to true completely wiped the `origin` header (at least in my development environment). I'm running a HonoJS backend with the cors plugin, so I think that's causing my /api/auth routes to 403 FORBIDDEN when I run the expo plugin with this setting. Instead, I wrote a middleware function to manually replace the incoming request's headers with the `expo-origin` header sent by the client. ```ts app.use("/api/auth/*", async (c, next) => { const ExpoOrigin = c.req.header("expo-origin"); if (ExpoOrigin) { const originalRequest = c.req.raw; // Create new headers const newHeaders = new Headers(originalRequest.headers); newHeaders.set("origin", ExpoOrigin); // Create a new Request with updated headers const newRequest = new Request(originalRequest, { headers: newHeaders, }); // Replace the request in context c.req.raw = newRequest; } await next(); }); ``` With this, you don't even need to set the `disableOriginOverride`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2201