[GH-ISSUE #4744] [BUG] better-auth/react with @better-auth/expo/client useSession infinite refetch on multiple Set-Cookie headers #10045

Closed
opened 2026-04-13 05:56:22 -05:00 by GiteaMirror · 4 comments
Owner

Originally created by @ArvidAnderson on GitHub (Sep 18, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/4744

Originally assigned to: @himself65 on GitHub.

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Setup better auth with Expo and a Hono backend.
  2. Make your backend return its own Set-Cookie header.
  3. Use the useSession hook from the auth client
  4. View network requests and see infinite refetching of useSession in dev and in prod until rate limit is hit

Current vs. Expected behavior

Following the steps in the previous section i expect the better-auth/react useSession with the @better-auth/expo/client plugin hook to properly handle multiple Set-Cookie headers according to RFC 6265 (Section 4.1, HTTP State Management Mechanism)

I also noted that it seems like if useSession response includes any other set cookie header than the better auth one, it will get into this refetching loop.

What version of Better Auth are you using?

1.3.11

System info

{
    "system": {
      "platform": "darwin",
      "arch": "arm64",
      "version": "Darwin Kernel Version 25.0.0: Mon Aug 25 21:17:56 PDT 2025; root:xnu-12377.1.9~3/RELEASE_ARM64_T6030",
      "release": "25.0.0",
      "cpuCount": 11,
      "cpuModel": "Apple M3 Pro",
      "totalMemory": "36.00 GB",
      "freeMemory": "1.93 GB"
    },
    "node": {
      "version": "v22.13.1",
      "env": "development"
    },
    "packageManager": {
      "name": "bun",
      "version": "1.2.21"
    },
    "frameworks": null,
    "databases": null,
    "betterAuth": {
      "version": "^1.3.11",
      "config": {
        "appName": "redacted",
        "basePath": "/api/auth",
        "secret": "",
        "trustedOrigins": [
          "redacted://"
        ],
        "emailAndPassword": {
          "enabled": false
        },
        "plugins": [
          {
            "name": "phone-number",
            "config": {
              "id": "phone-number",
              "endpoints": {},
              "schema": {
                "user": {
                  "fields": {
                    "phoneNumber": {
                      "type": "string",
                      "required": false,
                      "unique": true,
                      "sortable": true,
                      "returned": true
                    },
                    "phoneNumberVerified": {
                      "type": "boolean",
                      "required": false,
                      "returned": true,
                      "input": false
                    }
                  }
                }
              },
              "rateLimit": [
                {
                  "window": 60000,
                  "max": 10
                }
              ],
              "$ERROR_CODES": {
                "INVALID_PHONE_NUMBER": "Invalid phone number",
                "PHONE_NUMBER_EXIST": "Phone number already exists",
                "INVALID_PHONE_NUMBER_OR_PASSWORD": "[REDACTED]",
                "UNEXPECTED_ERROR": "Unexpected error",
                "OTP_NOT_FOUND": "OTP not found",
                "OTP_EXPIRED": "OTP expired",
                "INVALID_OTP": "Invalid OTP",
                "PHONE_NUMBER_NOT_VERIFIED": "Phone number not verified"
              }
            }
          },
          {
            "name": "expo",
            "config": {
              "id": "expo",
              "hooks": {
                "after": [
                  {}
                ]
              }
            }
          },
          {
            "name": "open-api",
            "config": {
              "id": "open-api",
              "endpoints": {}
            }
          }
        ],
        "advanced": {
          "database": {},
          "cookiePrefix": "redacted"
        },
        "telemetry": {
          "enabled": false
        }
      }
    }
  }

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

Client

Auth config (if applicable)

Server:

betterAuth({
    appName: "redacted",
    basePath: "/api/auth",
    database: drizzleAdapter(databaseClient, {
        provider: "pg",
        schema: {
            user: databaseClient._.fullSchema.user,
            session: databaseClient._.fullSchema.session,
            account: databaseClient._.fullSchema.account,
            verification: databaseClient._.fullSchema.verification,
        },
    }),
    trustedOrigins: [
        "redacted://",
    ],
    emailAndPassword: {
        enabled: false,
    },
    plugins: [
        phoneNumber({
            async sendOTP({ phoneNumber, code }, _request) {
                if (process.env.NODE_ENV === "production") {
                    const twilioClient = twilio(
                        process.env.TWILIO_ACCOUNT_SID,
                        process.env.TWILIO_AUTH_TOKEN,
                    );

                    await twilioClient.messages.create({
                        body: `Your verification code is: ${code}`,
                        to: phoneNumber,
                        messagingServiceSid:
                            process.env.TWILIO_AUTH_MESSAGING_SERVICE_SID,
                    });
                } else {
                    console.log({ phoneNumber, code });
                }
            },
            signUpOnVerification: {
                getTempEmail: (phoneNumber) => `${phoneNumber}@redacted.com`,
                getTempName: (phoneNumber) => phoneNumber,
            },
        }),
        expo(),
        openAPI({
            disableDefaultReference: true,
        }),
    ],
    advanced: {
        database: {
            generateId: () => crypto.randomUUID(),
        },
        cookiePrefix: "redacted",
    },
    telemetry: {
        enabled: false,
    },
});

Client:

createAuthClient({
	baseURL: "redacted",
	basePath: "/api/auth",
	fetchOptions: {
		timeout: 3000,
	},
	plugins: [phoneNumberClient(), expoClient(options.expo)],
});

Additional context

This was discovered when deploying to DigitalOcean App Platform that uses Cloudflare's bot protection services and sends a __cf_bm cookie. I have tried deploying to Azure Container Apps which are not behind Cloudflare's bot protection services and everything is working normally there. I have also tried locally with just a random set cookie header and it's causing the same issue.

Originally created by @ArvidAnderson on GitHub (Sep 18, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/4744 Originally assigned to: @himself65 on GitHub. ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Setup better auth with Expo and a Hono backend. 2. Make your backend return its own Set-Cookie header. 3. Use the useSession hook from the auth client 4. View network requests and see infinite refetching of useSession in dev and in prod until rate limit is hit ### Current vs. Expected behavior Following the steps in the previous section i expect the better-auth/react useSession with the @better-auth/expo/client plugin hook to properly handle multiple Set-Cookie headers according to [RFC 6265 (Section 4.1, HTTP State Management Mechanism)](https://datatracker.ietf.org/doc/html/rfc6265) I also noted that it seems like if useSession response includes any other set cookie header than the better auth one, it will get into this refetching loop. ### What version of Better Auth are you using? 1.3.11 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 25.0.0: Mon Aug 25 21:17:56 PDT 2025; root:xnu-12377.1.9~3/RELEASE_ARM64_T6030", "release": "25.0.0", "cpuCount": 11, "cpuModel": "Apple M3 Pro", "totalMemory": "36.00 GB", "freeMemory": "1.93 GB" }, "node": { "version": "v22.13.1", "env": "development" }, "packageManager": { "name": "bun", "version": "1.2.21" }, "frameworks": null, "databases": null, "betterAuth": { "version": "^1.3.11", "config": { "appName": "redacted", "basePath": "/api/auth", "secret": "", "trustedOrigins": [ "redacted://" ], "emailAndPassword": { "enabled": false }, "plugins": [ { "name": "phone-number", "config": { "id": "phone-number", "endpoints": {}, "schema": { "user": { "fields": { "phoneNumber": { "type": "string", "required": false, "unique": true, "sortable": true, "returned": true }, "phoneNumberVerified": { "type": "boolean", "required": false, "returned": true, "input": false } } } }, "rateLimit": [ { "window": 60000, "max": 10 } ], "$ERROR_CODES": { "INVALID_PHONE_NUMBER": "Invalid phone number", "PHONE_NUMBER_EXIST": "Phone number already exists", "INVALID_PHONE_NUMBER_OR_PASSWORD": "[REDACTED]", "UNEXPECTED_ERROR": "Unexpected error", "OTP_NOT_FOUND": "OTP not found", "OTP_EXPIRED": "OTP expired", "INVALID_OTP": "Invalid OTP", "PHONE_NUMBER_NOT_VERIFIED": "Phone number not verified" } } }, { "name": "expo", "config": { "id": "expo", "hooks": { "after": [ {} ] } } }, { "name": "open-api", "config": { "id": "open-api", "endpoints": {} } } ], "advanced": { "database": {}, "cookiePrefix": "redacted" }, "telemetry": { "enabled": false } } } } ``` ### Which area(s) are affected? (Select all that apply) Client ### Auth config (if applicable) ```typescript Server: betterAuth({ appName: "redacted", basePath: "/api/auth", database: drizzleAdapter(databaseClient, { provider: "pg", schema: { user: databaseClient._.fullSchema.user, session: databaseClient._.fullSchema.session, account: databaseClient._.fullSchema.account, verification: databaseClient._.fullSchema.verification, }, }), trustedOrigins: [ "redacted://", ], emailAndPassword: { enabled: false, }, plugins: [ phoneNumber({ async sendOTP({ phoneNumber, code }, _request) { if (process.env.NODE_ENV === "production") { const twilioClient = twilio( process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN, ); await twilioClient.messages.create({ body: `Your verification code is: ${code}`, to: phoneNumber, messagingServiceSid: process.env.TWILIO_AUTH_MESSAGING_SERVICE_SID, }); } else { console.log({ phoneNumber, code }); } }, signUpOnVerification: { getTempEmail: (phoneNumber) => `${phoneNumber}@redacted.com`, getTempName: (phoneNumber) => phoneNumber, }, }), expo(), openAPI({ disableDefaultReference: true, }), ], advanced: { database: { generateId: () => crypto.randomUUID(), }, cookiePrefix: "redacted", }, telemetry: { enabled: false, }, }); Client: createAuthClient({ baseURL: "redacted", basePath: "/api/auth", fetchOptions: { timeout: 3000, }, plugins: [phoneNumberClient(), expoClient(options.expo)], }); ``` ### Additional context This was discovered when deploying to DigitalOcean App Platform that uses Cloudflare's bot protection services and sends a __cf_bm cookie. I have tried deploying to Azure Container Apps which are not behind Cloudflare's bot protection services and everything is working normally there. I have also tried locally with just a random set cookie header and it's causing the same issue.
GiteaMirror added the locked label 2026-04-13 05:56:22 -05:00
Author
Owner

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

This infinite refetching loop happens because the @better-auth/expo client and useSession hook expect only the Better Auth session cookie in the Set-Cookie headers. When the response includes additional cookies (like Cloudflare’s __cf_bm or any unrelated Set-Cookie), the client’s cookie parsing logic treats the session as invalid or changed, which triggers a refetch. This is compounded by the fact that if the expected session cookie is missing or malformed, the client will keep retrying, leading to a loop and eventually rate limiting see analysis.

This is not currently handled per RFC 6265 in the expo client, and there’s no merged fix for this specific scenario yet. The codebase is modular enough that a fix could be introduced at the fetch/interceptor/plugin layer to filter or ignore non-auth cookies, but as of now, any extra Set-Cookie header can break the session validation loop see code analysis notes.

As a workaround, try to avoid having your backend or any proxy (like Cloudflare) inject unrelated Set-Cookie headers into auth responses, or consider patching your local client to filter out non-Better Auth cookies before session validation. If you control the backend, ensure only the Better Auth session cookie is set on auth endpoints, or handle unrelated cookies on separate endpoints if possible. If you’re behind a proxy that injects cookies, you may need to adjust your client’s cookie parsing logic to ignore unknown cookies until an upstream fix is available.

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:3306489088 --> @dosubot[bot] commented on GitHub (Sep 18, 2025): <!-- Answer --> This infinite refetching loop happens because the @better-auth/expo client and useSession hook expect only the Better Auth session cookie in the Set-Cookie headers. When the response includes additional cookies (like Cloudflare’s __cf_bm or any unrelated Set-Cookie), the client’s cookie parsing logic treats the session as invalid or changed, which triggers a refetch. This is compounded by the fact that if the expected session cookie is missing or malformed, the client will keep retrying, leading to a loop and eventually rate limiting [see analysis](https://github.com/better-auth/better-auth/issues/3180). This is not currently handled per RFC 6265 in the expo client, and there’s no merged fix for this specific scenario yet. The codebase is modular enough that a fix could be introduced at the fetch/interceptor/plugin layer to filter or ignore non-auth cookies, but as of now, any extra Set-Cookie header can break the session validation loop [see code analysis notes](https://github.com/better-auth/better-auth/issues/3180). As a workaround, try to avoid having your backend or any proxy (like Cloudflare) inject unrelated Set-Cookie headers into auth responses, or consider patching your local client to filter out non-Better Auth cookies before session validation. If you control the backend, ensure only the Better Auth session cookie is set on auth endpoints, or handle unrelated cookies on separate endpoints if possible. If you’re behind a proxy that injects cookies, you may need to adjust your client’s cookie parsing logic to ignore unknown cookies until an upstream fix is available. <!-- 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/441ec1db-c778-4ea4-8864-86836b7e0132?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/441ec1db-c778-4ea4-8864-86836b7e0132?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/441ec1db-c778-4ea4-8864-86836b7e0132?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/441ec1db-c778-4ea4-8864-86836b7e0132?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/441ec1db-c778-4ea4-8864-86836b7e0132?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/441ec1db-c778-4ea4-8864-86836b7e0132?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/441ec1db-c778-4ea4-8864-86836b7e0132?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/4744)
Author
Owner

@betomoedano commented on GitHub (Sep 25, 2025):

Same issue :(

<!-- gh-comment-id:3334363159 --> @betomoedano commented on GitHub (Sep 25, 2025): Same issue :(
Author
Owner

@ArvidAnderson commented on GitHub (Oct 6, 2025):

What is the status on this @himself65?

<!-- gh-comment-id:3371929437 --> @ArvidAnderson commented on GitHub (Oct 6, 2025): What is the status on this @himself65?
Author
Owner

@zouhangwithsweet commented on GitHub (Oct 17, 2025):

Same issue :(

<!-- gh-comment-id:3415914454 --> @zouhangwithsweet commented on GitHub (Oct 17, 2025): Same issue :(
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#10045