Skip to content
Control Plane Labs

419 Page Expired

Client error response defined by laravel.com.

Last updated July 27, 2026

Common causes at a glance

  • A form left open until the session cookie expired
  • A SPA sending a stale XSRF-TOKEN cookie value after re-authentication
  • SESSION_DOMAIN or SESSION_SECURE_COOKIE misconfigured so the cookie is never stored
  • A load balancer spreading requests across nodes with unshared file-based sessions

Reproduce it

curl -i -X POST -d 'name=new' https://app.example.com/settings/profile

What 419 Page Expired means

Laravel puts a CSRF middleware in front of every POST, PUT, PATCH and DELETE route by default. It compares the token in the request — a _token form field, or an X-CSRF-TOKEN or X-XSRF-TOKEN header — against the token stored in the user’s session. When they do not match, or the session no longer exists, the framework aborts with 419 and renders a “Page Expired” screen.

The phrase is a simplification aimed at end users. The honest description is “the token you sent does not belong to the session you are in”.

There is no RFC

419 is unassigned in the IANA registry. It comes from Laravel’s own HTTP exception handling, described in the CSRF protection documentation, which notes the 419 response is typically associated with CSRF token mismatches. No specification defines it, so RFC 9110 §15 applies instead: a client that does not recognise 419 treats it as an ordinary 400. Libraries will not retry or refresh anything for you, so a SPA must handle it explicitly. Monitoring buckets it under client errors, where a spike looks like user error but usually means a session or cookie misconfiguration.

What actually causes it

Two patterns account for nearly all of it. The first is the stale form: someone opens a page, goes to lunch, and submits after the session lifetime has passed. The second is a JavaScript client that read the XSRF-TOKEN cookie once at boot and kept using it after a login or session regeneration changed the underlying token. Behind both sits configuration: a wrong SESSION_DOMAIN, a Secure cookie served over plain HTTP, or sticky sessions turned off in front of nodes keeping sessions on local disk. Each produces a fresh empty session per request and a guaranteed mismatch.

How to debug it

Prove first that the session cookie survives the round trip. Post without a token, confirm you get 419, then check whether a cookie came back:

curl -i -c jar.txt -X POST -d 'name=new' https://app.example.com/settings/profile

If Set-Cookie is missing, or the cookie carries a Domain that does not match the host you called, the problem is configuration rather than token handling. Inspect the attributes with the header inspector.

If the cookie is fine, the SPA fix is to read XSRF-TOKEN immediately before each mutating request rather than caching it. Excluding routes from the middleware silences 419 by removing the protection, which is rarely what you want on a route that changes state.

Working examples

curl

# 1. Fetch the form and keep the session cookie.
curl -s -c jar.txt https://app.example.com/settings/profile > form.html

# 2. Extract the CSRF token Laravel embedded in the form.
TOKEN=$(grep -o 'name="_token" value="[^"]*"' form.html | cut -d'"' -f4)

# 3. Post with both cookie jar and token. Dropping either one yields 419.
curl -i -b jar.txt -X POST \
  -d "_token=$TOKEN" -d 'name=new' \
  https://app.example.com/settings/profile

Python (requests)

import re
import requests

s = requests.Session()  # the session object keeps the Laravel session cookie

page = s.get("https://app.example.com/settings/profile", timeout=10)
token = re.search(r'name="_token" value="([^"]+)"', page.text).group(1)

resp = s.post(
    "https://app.example.com/settings/profile",
    data={"_token": token, "name": "new"},
    timeout=10,
)
print(resp.status_code)  # 419 if the token or cookie is stale

# Without the session cookie the mismatch is guaranteed:
print(requests.post("https://app.example.com/settings/profile",
                    data={"name": "new"}, timeout=10).status_code)

Node (fetch)

// Read XSRF-TOKEN per request. Caching it is the usual source of 419 in a SPA.
function xsrfToken() {
  const hit = document.cookie.match(/(?:^|; )XSRF-TOKEN=([^;]*)/);
  return hit ? decodeURIComponent(hit[1]) : "";
}

const res = await fetch("https://app.example.com/settings/profile", {
  method: "POST",
  credentials: "include", // without this the session cookie never goes out
  headers: {
    "X-XSRF-TOKEN": xsrfToken(),
    "Content-Type": "application/json",
    Accept: "application/json",
  },
  body: JSON.stringify({ name: "new" }),
});

if (res.status === 419) {
  // Refresh the CSRF cookie, then replay once. Do not loop.
  await fetch("https://app.example.com/sanctum/csrf-cookie", { credentials: "include" });
}
console.log(res.status);

Reference and tooling

The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.

Other 4xx codes