Fix ActionController::InvalidAuthenticityToken: Can't verify CSRF token authenticity. in Rails
This error occurs when a form submission or AJAX request does not include a valid CSRF token. Rails uses CSRF tokens to prevent cross-site request forgery attacks. Ensure your layout includes csrf_meta_tags, your forms use form_with, and your AJAX requests include the token in the X-CSRF-Token header.
Reading the Stack Trace
Here's what each line means:
- actionpack (7.1.3) lib/action_controller/metal/request_forgery_protection.rb:245:in `handle_unverified_request': Rails detected the CSRF token is missing or invalid and raises the error.
- actionpack (7.1.3) lib/action_controller/metal/request_forgery_protection.rb:282:in `verify_authenticity_token': The before_action callback that verifies the token on every non-GET request.
- app/controllers/posts_controller.rb:15:in `create': The create action was the target of the POST request that failed CSRF verification.
Common Causes
1. Missing CSRF meta tag in layout
The application layout does not include csrf_meta_tags, so AJAX requests cannot read the token.
<!-- app/views/layouts/application.html.erb -->
<head>
<title>MyApp</title>
<!-- csrf_meta_tags is missing -->
</head>
2. AJAX request without CSRF token header
A fetch or XMLHttpRequest call does not include the X-CSRF-Token header.
fetch('/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Hello' })
// Missing X-CSRF-Token header
});
3. Session expired or cookie cleared
The user's session containing the CSRF token expired but the page with the form is still open.
# User opens form, walks away for hours
# Session expires, they come back and submit
# The token in the form no longer matches the session
The Fix
Read the CSRF token from the meta tag that csrf_meta_tags injects into the layout, then include it in the X-CSRF-Token header of every non-GET AJAX request.
fetch('/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Hello' })
});
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
fetch('/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ title: 'Hello' })
});
Testing the Fix
require 'rails_helper'
RSpec.describe PostsController, type: :controller do
describe 'POST #create' do
it 'succeeds with a valid CSRF token' do
post :create, params: { post: { title: 'Hello', body: 'World' } }
expect(response).to have_http_status(:created)
end
it 'rejects request without CSRF token' do
ActionController::Base.allow_forgery_protection = true
expect {
post :create, params: { post: { title: 'Hello' } }
}.to raise_error(ActionController::InvalidAuthenticityToken)
ActionController::Base.allow_forgery_protection = false
end
end
end
Run your tests:
bundle exec rspec spec/controllers/posts_controller_spec.rb
Pushing Through CI/CD
git checkout -b fix/rails-csrf-token,git add app/views/layouts/application.html.erb app/javascript/api.js,git commit -m "fix: include CSRF token in AJAX request headers",git push origin fix/rails-csrf-token
Your CI config should look something like this:
name: CI
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
ports: ['5432:5432']
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
- run: bin/rails db:setup
- run: bundle exec rspec
The Full Manual Process: 18 Steps
Here's every step you just went through to fix this one bug:
- Notice the error alert or see it in your monitoring tool
- Open the error dashboard and read the stack trace
- Identify the file and line number from the stack trace
- Open your IDE and navigate to the file
- Read the surrounding code to understand context
- Reproduce the error locally
- Identify the root cause
- Write the fix
- Run the test suite locally
- Fix any failing tests
- Write new tests covering the edge case
- Run the full test suite again
- Create a new git branch
- Commit and push your changes
- Open a pull request
- Wait for code review
- Merge and deploy to production
- Monitor production to confirm the error is resolved
Total time: 30-60 minutes. For one bug.
Or Let bugstack Fix It in Under 2 minutes
Every step above? bugstack does it automatically.
Step 1: Install the SDK
gem install bugstack
Step 2: Initialize
require 'bugstack'
Bugstack.init(api_key: ENV['BUGSTACK_API_KEY'])
Step 3: There is no step 3.
bugstack handles everything from here:
- Captures the stack trace and request context
- Pulls the relevant source files from your GitHub repo
- Analyzes the error and understands the code context
- Generates a minimal, verified fix
- Runs your existing test suite
- Pushes through your CI/CD pipeline
- Deploys to production (or opens a PR for review)
Time from error to fix deployed: Under 2 minutes.
Human involvement: zero.
Try bugstack Free →No credit card. 5-minute setup. Cancel anytime.
Deploying the Fix (Manual Path)
- Verify csrf_meta_tags is in the layout head.
- Confirm all AJAX requests include the X-CSRF-Token header.
- Run controller and integration tests.
- Open a pull request for review.
- Merge and verify form submissions work in staging.
Frequently Asked Questions
BugStack runs the fix through your existing test suite, generates additional edge-case tests, and validates that no other components are affected before marking it safe to deploy.
BugStack never pushes directly to production. Every fix goes through a pull request with full CI checks, so your team can review it before merging.
Yes, use skip_before_action :verify_authenticity_token for API controllers that use token-based authentication instead of cookies. But never disable it for session-based controllers.
If you deploy to multiple servers, ensure they share the same secret_key_base. Different keys generate different tokens, causing mismatches when load-balanced.