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/profileWhat 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
- 400 Bad Request
- 401 Unauthorized
- 402 Payment Required
- 403 Forbidden
- 404 Not Found
- 405 Method Not Allowed
- 406 Not Acceptable
- 407 Proxy Authentication Required
- 408 Request Timeout
- 409 Conflict
- 410 Gone
- 411 Length Required
- 412 Precondition Failed
- 413 Content Too Large
- 414 URI Too Long
- 415 Unsupported Media Type
- 416 Range Not Satisfiable
- 417 Expectation Failed
- 418 (Unused)
- 421 Misdirected Request
- 422 Unprocessable Content
- 423 Locked
- 424 Failed Dependency
- 425 Too Early
- 426 Upgrade Required
- 428 Precondition Required
- 429 Too Many Requests
- 431 Request Header Fields Too Large
- 444 No Response
- 451 Unavailable For Legal Reasons
- 494 Request Header Too Large
- 495 SSL Certificate Error
- 496 SSL Certificate Required
- 497 HTTP Request Sent to HTTPS Port
- 499 Client Closed Request