React fetch OPTIONS Fails on /api/auth (Status 0) #984

Closed
opened 2026-03-13 08:15:20 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @yasir-khilji-64 on GitHub (Apr 5, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

I'm experiencing an issue with better‑auth on an Express.js API when accessed from a React client. Although the setup works as expected in Postman/Thunder Client, requests originating from a React client (using fetch) result in a network error (status 0) and no response body.

According to the better‑auth documentation, the auth handler must be mounted before any JSON-parsing middleware (i.e. before express.json()) so that better‑auth can access the raw request body. My current middleware order is as follows:

// src/app.ts
const app: Application = express();

app.use(cors());
app.use(helmet());

// Mount auth routes before JSON parsing middleware
app.all('/api/auth/*', (req: Request, res: Response) => {
  console.log(req);
  const auth = AuthProvider.GetInstance.GetAuthProviderInstance;
  return toNodeHandler(auth)(req, res);
});

// Apply JSON middleware for other routes
app.use(express.json());

The React client is set up using the better‑auth React client:

// lib/auth-client.ts
import { createAuthClient } from "better-auth/react";

const authClient = createAuthClient({
    baseURL: 'http://localhost:3030',
});

export { authClient };

And in a component, I call the sign‑up method:

// App.tsx
const { signUp } = authClient;

const handleSignUp = async () => {
  try {
    const response = await signUp.email(
      {
        email: 'test@test.com',
        name: 'Test',
        password: 'secret@123',
        callbackURL: '/',
      },
      {
        onRequest: (ctx) => {
          console.log(ctx);
        },
      }
    );
    console.log(response);
  } catch (error) {
    console.error(error);
  }
};

Steps to Reproduce:

  1. Set up the Express API with better‑auth as shown above.
  2. Use the better‑auth React client to call an endpoint (e.g., POST /api/auth/sign-up/email).
  3. Verify that Postman requests work but React (fetch‑initiated) requests return a network error (status 0) with an empty response.
  4. Observe that OPTIONS preflight requests return 204, yet the subsequent POST fails.

Current vs. Expected behavior

Current Behavior:

  • When the request is initiated from React using fetch, the network tab in the browser shows a POST to http://localhost:3030/api/auth/sign-up/email that returns a status of 0 and an empty body.
  • This issue appears to be tied to the preflight OPTIONS request or differences in how fetch sends its payload compared to Postman.

Expected Behavior:

  • The React client should successfully send requests to the /api/auth endpoints and receive valid responses, just as Postman does.
  • better‑auth should have access to the raw request body without being affected by subsequent JSON-parsing middleware.

What version of Better Auth are you using?

1.2.5

Provide environment information

- Node.js: 20.19.0
- npm: 10.8.2
- Express: 4.21.2
- better‑auth: 1.2.5
- React: 19.0.0 (Vite)
- OS: Ubuntu 24.04

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

Backend, Client

Auth config (if applicable)

mport { betterAuth } from 'better-auth';
// eslint-disable-next-line import/no-unresolved
import { mongodbAdapter } from 'better-auth/adapters/mongodb';      

    this.authInstance = betterAuth({
        database: mongodbAdapter(AuthProvider.database.getClient()),
        emailAndPassword: {
          enabled: true,
          requireEmailVerification: false,
        },
      });

Additional context

Help Needed

I'm looking for guidance on whether this issue might be caused

  • Differences in how browsers handle preflight OPTIONS requests or raw request bodies when using
  • Additional configuration required to correctly isolate the auth endpoints from the JSON parsing
  • Any known issues or recommended adjustments for better‑auth in a React client environment.
Originally created by @yasir-khilji-64 on GitHub (Apr 5, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce I'm experiencing an issue with better‑auth on an Express.js API when accessed from a React client. Although the setup works as expected in Postman/Thunder Client, requests originating from a React client (using fetch) result in a network error (status 0) and no response body. According to the better‑auth documentation, the auth handler must be mounted before any JSON-parsing middleware (i.e. before express.json()) so that better‑auth can access the raw request body. My current middleware order is as follows: ``` // src/app.ts const app: Application = express(); app.use(cors()); app.use(helmet()); // Mount auth routes before JSON parsing middleware app.all('/api/auth/*', (req: Request, res: Response) => { console.log(req); const auth = AuthProvider.GetInstance.GetAuthProviderInstance; return toNodeHandler(auth)(req, res); }); // Apply JSON middleware for other routes app.use(express.json()); ``` The React client is set up using the better‑auth React client: ``` // lib/auth-client.ts import { createAuthClient } from "better-auth/react"; const authClient = createAuthClient({ baseURL: 'http://localhost:3030', }); export { authClient }; ``` And in a component, I call the sign‑up method: ``` // App.tsx const { signUp } = authClient; const handleSignUp = async () => { try { const response = await signUp.email( { email: 'test@test.com', name: 'Test', password: 'secret@123', callbackURL: '/', }, { onRequest: (ctx) => { console.log(ctx); }, } ); console.log(response); } catch (error) { console.error(error); } }; ``` ### Steps to Reproduce: 1. Set up the Express API with better‑auth as shown above. 2. Use the better‑auth React client to call an endpoint `(e.g., POST /api/auth/sign-up/email)`. 3. Verify that Postman requests work but React (fetch‑initiated) requests return a network error (status 0) with an empty response. 4. Observe that OPTIONS preflight requests return 204, yet the subsequent POST fails. ### Current vs. Expected behavior ### Current Behavior: - When the request is initiated from React using fetch, the network tab in the browser shows a POST to `http://localhost:3030/api/auth/sign-up/email` that returns a status of 0 and an empty body. - This issue appears to be tied to the preflight OPTIONS request or differences in how fetch sends its payload compared to Postman. ### Expected Behavior: - The React client should successfully send requests to the `/api/auth` endpoints and receive valid responses, just as Postman does. - better‑auth should have access to the raw request body without being affected by subsequent JSON-parsing middleware. ### What version of Better Auth are you using? 1.2.5 ### Provide environment information ```bash - Node.js: 20.19.0 - npm: 10.8.2 - Express: 4.21.2 - better‑auth: 1.2.5 - React: 19.0.0 (Vite) - OS: Ubuntu 24.04 ``` ### Which area(s) are affected? (Select all that apply) Backend, Client ### Auth config (if applicable) ```typescript mport { betterAuth } from 'better-auth'; // eslint-disable-next-line import/no-unresolved import { mongodbAdapter } from 'better-auth/adapters/mongodb'; this.authInstance = betterAuth({ database: mongodbAdapter(AuthProvider.database.getClient()), emailAndPassword: { enabled: true, requireEmailVerification: false, }, }); ``` ### Additional context ### Help Needed I'm looking for guidance on whether this issue might be caused - Differences in how browsers handle preflight OPTIONS requests or raw request bodies when using - Additional configuration required to correctly isolate the auth endpoints from the JSON parsing - Any known issues or recommended adjustments for better‑auth in a React client environment.
Author
Owner

@yasir-khilji-64 commented on GitHub (Apr 8, 2025):

Closing this as I felt there were quite many issues with integrating it with Express and so, I feel this is not suitable for my use-case.

@yasir-khilji-64 commented on GitHub (Apr 8, 2025): Closing this as I felt there were quite many issues with integrating it with Express and so, I feel this is not suitable for my use-case.
Author
Owner

@SalonLynk commented on GitHub (Apr 8, 2025):

Closing this as I felt there were quite many issues with integrating it with Express and so, I feel this is not suitable for my use-case.

Can you keep it open? I'm having the same issue and would like an answer.

@SalonLynk commented on GitHub (Apr 8, 2025): > Closing this as I felt there were quite many issues with integrating it with Express and so, I feel this is not suitable for my use-case. Can you keep it open? I'm having the same issue and would like an answer.
Author
Owner

@siddharth-narayan commented on GitHub (Apr 14, 2025):

I am also having this issue with react. I'm using hono.

If the OPTIONS is handled within hono with cors, It seems like the client doesn't handle the 204 response properly

If the OPTIONS request is sent to the auth handler, it responds with a 404, which the client doesn't handle properly.

Repro:

import { Hono } from "hono";
import { cors } from 'hono/cors'
import { serve } from '@hono/node-server'

import { betterAuth } from "better-auth";
import Database from "better-sqlite3";

const app = new Hono();
const auth = betterAuth({
    database: new Database("./sqlite.db"),
    
    emailAndPassword: {
        enabled: true
    },
})

// With CORS - from https://github.com/LovelessCodes/hono-better-auth/
app.use(
    "/api/auth/**", // or replace with "*" to enable cors for all routes
    cors({
        origin: "localhost:3000",
        allowHeaders: ["Content-Type", "Authorization"],
        allowMethods: ["POST", "GET", "OPTIONS"],
        exposeHeaders: ["Content-Length"],
        maxAge: 600,
        credentials: true,
    }),
);
app.on(["GET", "POST"], "/api/auth/**", (c) => auth.handler(c.req.raw));

// Without CORS - forwarded directly
// app.all("/api/auth/**", (c) => auth.handler(c.req.raw));

serve(app);
@siddharth-narayan commented on GitHub (Apr 14, 2025): I am also having this issue with react. I'm using hono. If the OPTIONS is handled within hono with cors, It seems like the client doesn't handle the 204 response properly If the OPTIONS request is sent to the auth handler, it responds with a 404, which the client doesn't handle properly. Repro: ```ts import { Hono } from "hono"; import { cors } from 'hono/cors' import { serve } from '@hono/node-server' import { betterAuth } from "better-auth"; import Database from "better-sqlite3"; const app = new Hono(); const auth = betterAuth({ database: new Database("./sqlite.db"), emailAndPassword: { enabled: true }, }) // With CORS - from https://github.com/LovelessCodes/hono-better-auth/ app.use( "/api/auth/**", // or replace with "*" to enable cors for all routes cors({ origin: "localhost:3000", allowHeaders: ["Content-Type", "Authorization"], allowMethods: ["POST", "GET", "OPTIONS"], exposeHeaders: ["Content-Length"], maxAge: 600, credentials: true, }), ); app.on(["GET", "POST"], "/api/auth/**", (c) => auth.handler(c.req.raw)); // Without CORS - forwarded directly // app.all("/api/auth/**", (c) => auth.handler(c.req.raw)); serve(app); ```
Author
Owner

@siddharth-narayan commented on GitHub (Apr 14, 2025):

Haha, once again it seems like the problem was not better-auth! It was my CORS implementation. The OPTIONS http method is never sent by better-auth, but only the browser. In my case, the allowed origin (localhost) I set for cors did not match (127.0.0.1). @yasir-khilji-64 @SalonLynk, if you're still having this problem you should probably include CORS.

@siddharth-narayan commented on GitHub (Apr 14, 2025): Haha, once again it seems like the problem was not better-auth! It was my CORS implementation. The OPTIONS http method is never sent by better-auth, but only the browser. In my case, the allowed origin (localhost) I set for cors did not match (127.0.0.1). @yasir-khilji-64 @SalonLynk, if you're still having this problem you should probably include CORS.
Author
Owner

@Whbbit1999 commented on GitHub (Apr 25, 2025):

I am also having this issue with react. I'm using hono.我在 React 中也遇到了这个问题。我正在使用 hono。

If the OPTIONS is handled within hono with cors, It seems like the client doesn't handle the 204 response properly如果 OPTIONS 在 hono 中使用 cors 处理,则客户端似乎无法正确处理 204 响应

If the OPTIONS request is sent to the auth handler, it responds with a 404, which the client doesn't handle properly.如果将 OPTIONS 请求发送到身份验证处理程序,它会以 404 响应,这是客户端无法正确处理的结果。

Repro:  复制:

import { Hono } from "hono";
import { cors } from 'hono/cors'
import { serve } from '@hono/node-server'

import { betterAuth } from "better-auth";
import Database from "better-sqlite3";

const app = new Hono();
const auth = betterAuth({
database: new Database("./sqlite.db"),

emailAndPassword: {
    enabled: true
},

})

// With CORS - from https://github.com/LovelessCodes/hono-better-auth/
app.use(
"/api/auth/", // or replace with "*" to enable cors for all routes
cors({
origin: "localhost:3000",
allowHeaders: ["Content-Type", "Authorization"],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length"],
maxAge: 600,
credentials: true,
}),
);
app.on(["GET", "POST"], "/api/auth/
", (c) => auth.handler(c.req.raw));

// Without CORS - forwarded directly
// app.all("/api/auth/**", (c) => auth.handler(c.req.raw));

serve(app);

Maybe you can set fetchOptions: { credentials: 'omit' } in your better-auth client

@Whbbit1999 commented on GitHub (Apr 25, 2025): > I am also having this issue with react. I'm using hono.我在 React 中也遇到了这个问题。我正在使用 hono。 > > If the OPTIONS is handled within hono with cors, It seems like the client doesn't handle the 204 response properly如果 OPTIONS 在 hono 中使用 cors 处理,则客户端似乎无法正确处理 204 响应 > > If the OPTIONS request is sent to the auth handler, it responds with a 404, which the client doesn't handle properly.如果将 OPTIONS 请求发送到身份验证处理程序,它会以 404 响应,这是客户端无法正确处理的结果。 > > Repro:  复制: > > import { Hono } from "hono"; > import { cors } from 'hono/cors' > import { serve } from '@hono/node-server' > > import { betterAuth } from "better-auth"; > import Database from "better-sqlite3"; > > const app = new Hono(); > const auth = betterAuth({ > database: new Database("./sqlite.db"), > > emailAndPassword: { > enabled: true > }, > }) > > // With CORS - from https://github.com/LovelessCodes/hono-better-auth/ > app.use( > "/api/auth/**", // or replace with "*" to enable cors for all routes > cors({ > origin: "localhost:3000", > allowHeaders: ["Content-Type", "Authorization"], > allowMethods: ["POST", "GET", "OPTIONS"], > exposeHeaders: ["Content-Length"], > maxAge: 600, > credentials: true, > }), > ); > app.on(["GET", "POST"], "/api/auth/**", (c) => auth.handler(c.req.raw)); > > // Without CORS - forwarded directly > // app.all("/api/auth/**", (c) => auth.handler(c.req.raw)); > > serve(app); Maybe you can set `fetchOptions: { credentials: 'omit' }` in your better-auth client
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#984