How It Works Features Pricing Blog Error Guides
Log In Start Free Trial
Rails · Ruby

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

ActionController::InvalidAuthenticityToken (Can't verify CSRF token authenticity.): actionpack (7.1.3) lib/action_controller/metal/request_forgery_protection.rb:245:in `handle_unverified_request' actionpack (7.1.3) lib/action_controller/metal/request_forgery_protection.rb:282:in `verify_authenticity_token' activesupport (7.1.3) lib/active_support/callbacks.rb:403:in `block in make_lambda' app/controllers/posts_controller.rb:15:in `create' actionpack (7.1.3) lib/action_controller/metal/basic_implicit_render.rb:6:in `send_action'

Here's what each line means:

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.

Before (broken)
fetch('/posts', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title: 'Hello' })
});
After (fixed)
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:

  1. Notice the error alert or see it in your monitoring tool
  2. Open the error dashboard and read the stack trace
  3. Identify the file and line number from the stack trace
  4. Open your IDE and navigate to the file
  5. Read the surrounding code to understand context
  6. Reproduce the error locally
  7. Identify the root cause
  8. Write the fix
  9. Run the test suite locally
  10. Fix any failing tests
  11. Write new tests covering the edge case
  12. Run the full test suite again
  13. Create a new git branch
  14. Commit and push your changes
  15. Open a pull request
  16. Wait for code review
  17. Merge and deploy to production
  18. 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:

  1. Captures the stack trace and request context
  2. Pulls the relevant source files from your GitHub repo
  3. Analyzes the error and understands the code context
  4. Generates a minimal, verified fix
  5. Runs your existing test suite
  6. Pushes through your CI/CD pipeline
  7. 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)

  1. Verify csrf_meta_tags is in the layout head.
  2. Confirm all AJAX requests include the X-CSRF-Token header.
  3. Run controller and integration tests.
  4. Open a pull request for review.
  5. 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.