# Can't verify CSRF token authenticity: Completed 422 Unprocessable Entity

## TL;DR
The request is missing a valid CSRF token, or the token doesn't match the session, so the framework rejects it with a 422. Send a fresh token in the X-CSRF-Token header or the authenticity_token form field. If the caller is an API client rather than a browser form, the real fix is switching that controller to null_session or header-based auth.

```text
Can't verify CSRF token authenticity: Completed 422 Unprocessable Entity
```

## Use this when
- A Rails form POST, PATCH, or DELETE returns 422 with this exact log line
- A fetch or XHR request from your own frontend fails while the same action works from a full page load
- Logins, signups, or settings saves break right after a deploy or a session change
- An agent driving a browser or HTTP client hits 422 on state-changing requests

## Not for this skill when
- The 422 comes from a validation error, not CSRF (check the log line first)
- You are building a pure JSON API with no browser sessions; use token auth instead
- The error is about CORS preflight failures
- You want to disable CSRF protection globally; that is almost never the answer

## Steps
1. Confirm CSRF is the actual cause. Search the server log for the exact line:
```
grep "Can't verify CSRF token authenticity" log/production.log | tail -5
```
Expected: matching lines at the same timestamps as the 422s. If there are none, the 422 is coming from something else.

2. Check that the page ships a fresh token. The layout needs the meta tags, and forms need the hidden field:
```
grep -r "csrf_meta_tags" app/views/layouts/
```
Expected: at least one layout includes it. If your frontend is React or Vue, read the token from the meta tag and attach it to every mutating request:
```js
const el = document.querySelector('meta[name="csrf-token"]');
fetch("/settings", {
  method: "PATCH",
  headers: { "X-CSRF-Token": el.content, "Content-Type": "application/json" },
  body: JSON.stringify({ name: "new name" })
});
```
Expected: the request returns 200 or 302, not 422.

3. Check whether the session is being reset between page render and submit. Common causes: signing in on one tab invalidates the other tab's token, or the session store changed on deploy. After sign-in, force a full page reload so the token is fresh.
Expected: a full refresh before the action makes the 422 go away.

4. If the caller is an API client or another service, stop sending CSRF tokens and change the forgery strategy for that controller:
```ruby
class Api::BaseController # inherits ActionController::API, no session
  # so no CSRF check
end
```
For a mixed controller:
```ruby
protect_from_forgery with: :null_session, only: [:create]
```
Expected: API calls succeed without a token, while browser forms still enforce it.

5. Verify end to end: submit the form or replay the request, then confirm no new 422 lines appear in the log.
Expected: clean log, successful response.

### Variant: ActionController::InvalidAuthenticityToken in Rails 7 with Turbo
Turbo Drive requests need the token in the header, and Turbo expects a response it can handle. Same fix: meta tag plus X-CSRF-Token header. If you disabled Turbo on a form with data-turbo set to false, the token still has to be present in the form body.

### Variant: 422 on every POST after enabling a CDN or page cache
Cached pages serve stale tokens from before the session rotated. Exclude pages with forms from the cache, or fetch the token from an uncached endpoint and inject it before submit.

### Variant: CSRF failures only in the test suite
Test requests don't run the browser session setup. In request specs, sign in through the session helper first or pass the token explicitly. Don't turn off forgery protection in the test env just to make red go green.

## Why this happens
CSRF protection works by comparing a token tied to the user's session against a token submitted with the request. Any mismatch, a missing token, a token from an older session, or a session that got reset server-side, reads as a possibly forged request, so the framework rejects it with a 422. After deploys, session secret changes or store swaps silently invalidate every outstanding token.

## Edge cases and pitfalls
- protect_from_forgery with the exception strategy in an API controller raises instead of returning 422; use null_session there.
- If you use cookie store sessions and rotate the secret on every deploy, expect this error after every deploy until the secret is stable.
- Direct-to-S3 uploads followed by a form submit can double-submit stale tokens; refresh the token after the upload completes.
- Browser extensions that strip headers can remove X-CSRF-Token; if only some users hit it, ask about extensions.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_atx7Rms42qcKvPQ-D0Ggfg
