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

  1. Include the current CSRF token in requests: header 'X-CSRF-Token' from the session (or the csrfToken exposed by Flarum's frontend payload).
  2. Refresh the page / re-fetch a session and its token when it has expired, then retry the request once.
  3. 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

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


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)