flarum/framework · error · TokenMismatchException
CSRF token did not match
Error message
CSRF token did not match
What it means
CheckCsrfToken::process throws TokenMismatchException('CSRF token did not match') when a non-safe request's session CSRF token doesn't match the token submitted with the request. Flarum guards all state-changing requests against cross-site request forgery; only requests whose tokens match proceed down the middleware stack.
Solutions
- Include the current CSRF token in requests: header 'X-CSRF-Token' from the session (or the csrfToken exposed by Flarum's frontend payload).
- Refresh the page / re-fetch a session and its token when it has expired, then retry the request once.
- For server-to-server calls, authenticate via API keys (e.g. the api_token mechanism) instead of session cookies, which don't need CSRF tokens.
Example fix
// before
fetch('/api/discussions', { method: 'POST', body: data });
// after
fetch('/api/discussions', {
method: 'POST',
headers: { 'X-CSRF-Token': app.session.csrfToken },
body: data
}); Defensive patterns
Strategy: try-catch
Validate before calling
// ensure a token exists and is attached before sending
if (!app.session.csrfToken) await refreshSession();
const headers = { 'X-CSRF-Token': app.session.csrfToken }; Type guard
const hasCsrf = (req) => typeof req.headers?.['X-CSRF-Token'] === 'string' && req.headers['X-CSRF-Token'].length > 0;
Try / catch
try {
return await apiFetch('/api/discussions', { method: 'POST', headers, body });
} catch (e) {
if (isTokenMismatch(e)) { await refreshSession(); return retryWithFreshToken(); }
throw e;
} Prevention
- Always send X-CSRF-Token (or the csrf form param) with state-changing requests.
- Refresh the session/token after idle timeouts before retrying.
- Use API tokens instead of session cookies for non-browser clients.
- Avoid keeping stale tabs open across session rotation; reload before acting.
When it happens
Trigger: POST/PUT/DELETE requests missing the X-CSRF-Token header or csrfParam form field; requests after session expiry or regeneration; requests sent cross-origin (another site posting to the forum API); API calls that bypass Flarum's csrfToken acquisition step.
Common situations: Long-lived browser tabs where the session rotated while the page still holds the old token; custom API scripts posting directly without first fetching the CSRF token from the forum page/session; multiple tabs invalidating each other's tokens.
Related errors
- The forum is currently in maintenance mode.
- core.admin.appearance.custom_styles_cannot_use_less_features
- You cannot disable the default language pack!
- ValidationException (messages from password validator)
- custom_less
AI-assisted analysis of flarum/framework@4b939f6853 (2026-09-15).
Data as JSON: /api/errors/17e443e75b375890.
Report an issue: GitHub.
Appendix: source
Thrown at framework/core/src/Http/Middleware/CheckCsrfToken.php:43
public function process(Request $request, Handler $handler): Response
{
if (in_array($request->getAttribute('routeName'), $this->exemptRoutes, true)) {
return $handler->handle($request);
}
if (in_array($request->getMethod(), ['GET', 'HEAD', 'OPTIONS'])) {
return $handler->handle($request);
}
if ($request->getAttribute('bypassCsrfToken', false)) {
return $handler->handle($request);
}
if ($this->tokensMatch($request)) {
return $handler->handle($request);
}
throw new TokenMismatchException('CSRF token did not match');
}
private function tokensMatch(Request $request): bool
{
$expected = (string) $request->getAttribute('session')->token();
$provided = $request->getParsedBody()['csrfToken'] ??
$request->getHeaderLine('X-CSRF-Token');
return hash_equals($expected, $provided);
}
}
View on GitHub (pinned to 4b939f6853)