Dev.to WebDev 🛠 Dev 👁 0 📖 4 min read

Laravel Sanctum 419 Errors: A 5-Request Network Panel Triage

You set up Sanctum for your SPA, hit "Log in", and get 419 Page Expired (or a CORS error, or a 401 right after a "successful" login). Then the guessing starts: flip supports_credentials, add a domain somewhere, clear cac

You set up Sanctum for your SPA, hit "Log in", and get 419 Page Expired (or a CORS error, or a 401 right after a "successful" login). Then the guessing starts: flip supports_credentials, add a domain somewhere, clear cache, try again.

There's a faster way. Sanctum cookie auth is a chain of requests, and the browser's Network panel tells you exactly which link is broken. This is the triage order I use before touching any config.

How the chain is supposed to work

For a first-party SPA using Sanctum's cookie (stateful) mode:

  1. The SPA calls GET /sanctum/csrf-cookie. Laravel sets an XSRF-TOKEN cookie and a session cookie.
  2. The SPA sends POST /login. The HTTP client copies XSRF-TOKEN into an X-XSRF-TOKEN header and sends the cookies.
  3. Laravel validates the CSRF token, starts the session, and returns success.
  4. The SPA calls GET /api/user with the same cookies and gets the authenticated user.

A 419 means step 3 failed: Laravel didn't get a CSRF token that matches the session. Everything below is about finding out why.

Request 1: the preflight (OPTIONS)

Open DevTools, tick Preserve log, filter to Fetch/XHR, and log in once. If your SPA and API are on different origins, you'll see an OPTIONS request before the real one.

Check the response headers:

  • Access-Control-Allow-Origin must be your exact frontend origin (for example http://localhost:3000), never *. Browsers refuse credentials with a wildcard.
  • Access-Control-Allow-Credentials: true must be present.

If either is wrong, the fix is in config/cors.php:

'paths' => ['api/*', 'sanctum/csrf-cookie', 'login', 'logout'],
'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')],
'supports_credentials' => true,

A common miss: login isn't in paths, so /sanctum/csrf-cookie works but /login gets no CORS headers at all.

Request 2: /sanctum/csrf-cookie

You want a 204 with two Set-Cookie headers: XSRF-TOKEN and your session cookie (laravel_session or your app's name).

If the cookies are set but show a yellow warning icon in the Cookies tab, the browser rejected them. Hover the icon; it usually tells you why:

  • Domain mismatch: SESSION_DOMAIN doesn't cover the host. Use null for plain localhost, or .example.com when the SPA and API share a parent domain (app.example.com and api.example.com).
  • Secure over HTTP: SESSION_SECURE_COOKIE=true on a non-HTTPS local setup means the cookie is silently dropped.

Request 3: POST /login (where the 419 shows up)

Open the request headers and check two things:

  1. Is X-XSRF-TOKEN present? If not, your client isn't reading the cookie. With Axios 1.6.2 and later, cross-origin requests need both flags:
import axios from 'axios';

export const api = axios.create({
  baseURL: 'http://localhost:8000',
  withCredentials: true,
  withXSRFToken: true,
});

Use this one instance everywhere. A second bare axios.post() call somewhere in the codebase is a classic source of "it works on one page but not another".

  1. Is the Cookie header sent? If the header exists but cookies are missing, go back to Request 2. The browser never stored them.

If both are present and you still get 419, Laravel probably isn't treating the request as stateful. Check the next section.

The stateful-domain check

Sanctum only applies session and CSRF middleware to requests from domains listed in SANCTUM_STATEFUL_DOMAINS. The value is a host with port, without scheme:

SANCTUM_STATEFUL_DOMAINS=localhost:3000
SESSION_DOMAIN=null

And in Laravel 11+ make sure stateful API middleware is enabled in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->statefulApi();
})

Two traps I see all the time:

  • The frontend runs on localhost, but the API is called via 127.0.0.1. To the browser those are different sites, so cookies don't cross over. Pick one and use it everywhere.
  • You edited .env but config is cached. Run php artisan config:clear and restart the server.

Request 4: GET /api/user

If login returns 200/204 but this returns 401, the session cookie from login isn't coming back. Usually that's the same SESSION_DOMAIN or localhost vs 127.0.0.1 mismatch, or the route is protected with auth:sanctum while the request isn't recognised as stateful.

Request 5: the one you shouldn't make

Don't "fix" 419 by adding /login to the CSRF exceptions. It hides the symptom and removes protection from the route that needs it most. If the chain above checks out, Sanctum works with CSRF on.

Quick checklist

  • [ ] Access-Control-Allow-Origin is the exact SPA origin, not *
  • [ ] supports_credentials is true, and login/logout are in CORS paths
  • [ ] /sanctum/csrf-cookie sets both cookies with no browser warning
  • [ ] X-XSRF-TOKEN header and Cookie header are both on POST /login
  • [ ] SANCTUM_STATEFUL_DOMAINS includes host and port, no scheme
  • [ ] Same hostname everywhere (localhost or 127.0.0.1)
  • [ ] php artisan config:clear after every .env change

If you want to sanity-check what your API actually returns for a given origin before digging into Laravel config, I built a free CORS header tester and generator for exactly that.

For the full Laravel 13 walkthrough, including different-subdomain setups, Next.js server-context gotchas, and a long FAQ, see the complete guide: Laravel Sanctum CORS 419 Error Fix (Laravel 13) on Blogs World.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.