[Solved] Laravel CORS Error with React: Complete Fix

Fix the Laravel CORS error with React for good: config/cors.php, Sanctum cookies, preflight requests and the hidden 500 error that looks like a CORS issue.

A Laravel CORS error with React shows up as "Access to fetch at http://localhost:8000/api/... from origin http://localhost:5173 has been blocked by CORS policy". It is one of the first walls people hit when they split a project into a React front-end and a Laravel API, and it keeps coming back in production when domains change.

I have set this up on many React and Laravel projects, from small dashboards to multi-tenant CRMs. In this guide I explain what CORS actually is in plain words, show the exact Laravel 11 configuration that fixes it, cover the Sanctum cookie setup for SPAs, and list the errors that look like CORS but are something else entirely. The key idea to keep in mind: the fix is always on the server, never in React.

Prerequisites

  • Laravel 11 API (PHP 8.3) running on its own origin, for example http://localhost:8000
  • React 18 app built with Vite, for example on http://localhost:5173
  • Axios or fetch for HTTP calls
  • Optional: Laravel Sanctum if you use cookie-based authentication

What the CORS error really means

An origin is the combination of scheme, host and port. http://localhost:5173 and http://localhost:8000 are different origins because the port differs. Browsers allow a page to send a request to another origin, but they refuse to let JavaScript read the response unless the server replies with an Access-Control-Allow-Origin header that names the page's origin.

For anything other than simple GET or form POST requests, for example a JSON body or an Authorization header, the browser first sends an OPTIONS preflight request. The server must answer that preflight with the right headers, or the real request is never sent.

Postman and curl never show CORS errors because they are not browsers. That is why "it works in Postman" tells you nothing here.

Step 1: Publish and configure config/cors.php in Laravel 11

Laravel ships with the HandleCors middleware enabled globally. In Laravel 11 the config file is not published by default, so publish it first, then edit it.

php artisan config:publish cors

Step 2: Set allowed origins from the environment

Keep origins in .env so local, staging and production each allow only their own front-end. Do not use a wildcard together with credentials, browsers reject that combination.

// config/cors.php
return [
    'paths' => ['api/*', 'sanctum/csrf-cookie', 'login', 'logout'],

    'allowed_methods' => ['*'],

    'allowed_origins' => array_filter(explode(',', env('CORS_ALLOWED_ORIGINS', 'http://localhost:5173'))),

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 3600,

    'supports_credentials' => true,
];

# .env
CORS_ALLOWED_ORIGINS=http://localhost:5173,https://app.example.com
FRONTEND_URL=http://localhost:5173

Step 3: Clear the config cache

A cached config ignores every change you just made. This one catches people constantly, especially on servers where the deploy script runs config:cache.

php artisan config:clear
# in production, rebuild it after changes
php artisan config:cache

Step 4: Call the API correctly from React

Create one Axios instance with the base URL and credentials setting. withCredentials is only needed when you rely on cookies (Sanctum SPA auth). For token auth with a Bearer header it can stay false.

// src/lib/api.js
import axios from 'axios';

export const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL, // e.g. http://localhost:8000
  withCredentials: true,
  withXSRFToken: true,
  headers: { Accept: 'application/json' },
});

// src/hooks/useLeads.js
import { useEffect, useState } from 'react';
import { api } from '../lib/api';

export function useLeads() {
  const [leads, setLeads] = useState([]);
  const [error, setError] = useState(null);

  useEffect(() => {
    api.get('/api/leads')
      .then((res) => setLeads(res.data.data))
      .catch((err) => setError(err.response?.data?.message ?? err.message));
  }, []);

  return { leads, error };
}

Step 5: Laravel Sanctum CORS setup for a React SPA

With cookie-based Sanctum auth, CORS alone is not enough. The browser must be allowed to send cookies across origins, and Sanctum must recognise the front-end as stateful. The front-end and API should share a top-level domain in production, for example app.example.com and api.example.com.

Before logging in, React must call /sanctum/csrf-cookie once so Laravel sets the XSRF-TOKEN cookie.

# .env (API)
SANCTUM_STATEFUL_DOMAINS=localhost:5173,app.example.com
SESSION_DOMAIN=.example.com   # use localhost in local dev
SESSION_SECURE_COOKIE=true    # false on local http

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->statefulApi();
})

// React login
await api.get('/sanctum/csrf-cookie');
await api.post('/login', { email, password });
const { data: user } = await api.get('/api/user');

Optional: Avoid CORS in development with a Vite proxy

During development you can make the browser think the API is on the same origin by proxying through Vite. This removes CORS from local work entirely, but you still need the server config for production.

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': 'http://localhost:8000',
      '/sanctum': 'http://localhost:8000',
    },
  },
});

Common CORS errors in Laravel and what they really mean

  • No Access-Control-Allow-Origin header is present: often the request crashed with a 500 error, error responses from a fatal error or a die() call carry no CORS headers, check storage/logs/laravel.log
  • The value of Access-Control-Allow-Origin must not be the wildcard when credentials mode is include: replace * with explicit origins
  • Response to preflight request does not pass access control check: the OPTIONS request hit a route that redirects (for example auth redirect to /login) or Nginx returned 405
  • Origin not allowed in production only: the production URL is missing from CORS_ALLOWED_ORIGINS or the config cache is stale
  • Trailing slash mismatch: https://app.example.com/ is not the same string as https://app.example.com
  • Duplicate Access-Control-Allow-Origin headers: Nginx and Laravel both add them, remove the Nginx add_header lines

Debugging CORS step by step in DevTools

Open the Network tab, enable Preserve log, and find the failing request. If there is an OPTIONS request, inspect it first. Look at the status code and the response headers.

If the status is 500, 404 or 302, the problem is not CORS configuration at all. Fix the route or the crash and the CORS message disappears. If the status is 204 or 200 but Access-Control-Allow-Origin is missing, the path is not covered by the paths array or the config cache is stale. If the header is there but has the wrong value, the origin in .env does not exactly match.

Security best practices

  • Never use allowed_origins ['*'] on an API that uses cookies or sessions
  • List exact production origins rather than broad regex patterns
  • Keep paths limited to api/* and the Sanctum routes instead of every route
  • Remember CORS is not authentication, it only controls which browsers can read responses, protect routes with Sanctum or policies
  • Do not try to fix CORS from React with mode no-cors, it hides the response from your code

Conclusion

A Laravel CORS error with React comes down to the API not returning the right headers for your front-end's origin. Publish config/cors.php, list your origins from .env, clear the config cache, and set up Sanctum properly if you use cookies. When the error persists, check the Network tab for a hidden 500 or redirect before touching the config again. That has resolved every CORS issue I have met between React and Laravel.

Frequently asked questions

How do I fix the CORS error between React and Laravel?

Publish config/cors.php with php artisan config:publish cors, add your React origin to allowed_origins and run php artisan config:clear. The fix is always on the Laravel side.

Where is the CORS config in Laravel 11?

Laravel 11 does not publish config/cors.php by default. Run php artisan config:publish cors to create it, the HandleCors middleware is already enabled globally.

Why do I get a CORS error only when the server returns an error?

Fatal errors and some crashes return a response without CORS headers, so the browser reports a CORS problem. Check laravel.log for the real exception.

Can I fix CORS in React instead of Laravel?

No. CORS is enforced by the browser based on the server's response headers. In development you can avoid it with a Vite proxy, but production needs the Laravel config.

Do I need supports_credentials for Laravel Sanctum?

Yes, when using cookie-based SPA authentication. It must be true and allowed_origins must list exact origins, not a wildcard.