Getting CSRF token mismatch / 419 when making POST call to backend

Topic summary

Issue: A developer using the React/PHP Shopify App template encountered a 419 CSRF token mismatch error when making POST requests from the React frontend to PHP backend routes defined in web.php. GET requests worked fine, but POST calls consistently failed with a Symfony\Component\HttpKernel\Exception\HttpException.

Root Cause: The Laravel backend was rejecting POST requests due to missing or invalid CSRF tokens, which are required for state-changing operations.

Solution: The issue was resolved by following guidance from an external blog post about CSRF handling in Shopify apps. The fix involved:

  • Using Shopify’s authenticatedFetch utility from @shopify/app-bridge/utilities
  • Including the CSRF token in request headers as 'X-CSRF-TOKEN'
  • Obtaining the token via a useCsrf() hook or similar mechanism
  • Properly configuring the authenticated fetch instance with useAppBridge()

Status: Resolved. The developer confirmed the solution worked after implementing the CSRF token handling in their React component’s POST request logic.

Summarized with AI on November 12. AI used: claude-sonnet-4-5-20250929.

I’m using the React/PHP Shopify App template. I’m trying to connect the React frontend to the PHP backend using web.php to define my routes and using the php Controller classes outlined in the template.

GET requests to my own Controller endpoints are working fine, but when I try to make a POST, I get the following http response:
Response code: 419

"message": "CSRF token mismatch.",
    "exception": "Symfony\\Component\\HttpKernel\\Exception\\HttpException",
    "file": "/Users/ryan/Repos/ddm-spring-calculator/web/vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php",
    "line": 389,
    "trace": [
        {
            "file": "/Users/ryan/Repos/ddm-spring-calculator/web/vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php",
            "line": 332,
            "function": "prepareException",
            "class": "Illuminate\\Foundation\\Exceptions\\Handler",
            "type": "->"
        },
        {
            "file": "/Users/ryan/Repos/ddm-spring-calculator/web/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php",
            "line": 51,
            "function": "render",
            "class": "Illuminate\\Foundation\\Exceptions\\Handler",
            "type": "->"
        },
        {
            "file": "/Users/ryan/Repos/ddm-spring-calculator/web/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 172,
            "function": "handleException",
            "class": "Illuminate\\Routing\\Pipeline",
            "type": "->"
        },
etc...

In my web.php file, I have this line (along with everything else that comes in the template:

Route::post('/api/springs', [SpringsController::class, 'getSprings']);

My react code looks like this:

const fetchSprings = async () => {
        const response = await httpService.post(`/springs`, {1: "ryan"});
        setResultText("The response is: " + response[1]);
    };
import { useAppBridge } from "@shopify/app-bridge-react";
import { authenticatedFetch } from "@shopify/app-bridge/utilities";

class HttpService {
    private authFetch;

    constructor() {
        this.authFetch = authenticatedFetch(useAppBridge());
    }
  
    public async post(route: string, data: any) {
        try {
            const response = await this.authFetch(this.getFullRoute(route), {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(data),
            });

            if (!response.ok) {
                throw new Error(`HTTP error! Status: ${response.status}`);
            }

            const responseData = await response.json();
            console.log(responseData); // Handle the response as needed

            return responseData; // You may want to return the data for further use
        } catch (error) {
            console.error('Error making POST request:', error);
            throw error; // Rethrow the error or handle it as needed
        }
    };

    private getFullRoute(route: string): string {
        const normalizedRoute: string = route.startsWith("/api") ? route : `/api${route}`;
        return `${normalizedRoute}?shop=quickstart-f783802a.myshopify.com`;
    }
}

And my SpringsController.php looks like this:

json()->all();

        // Access the value using the key '1'
        $value = $jsonData['1'];

        $data = array();
        $data[1] = $value;

        return response()->json($data);
    }
}

In my .env file, I have these values (obfuscated here):

APP_NAME="DDM Spring Calculator"
APP_ENV=local
APP_KEY=base64:***
APP_DEBUG=true
APP_URL=https://quickstart-f783802a.myshopify.com

LOG_CHANNEL=stack
LOG_LEVEL=debug

## sqlite ##
DB_CONNECTION=sqlite
DB_FOREIGN_KEYS=true
DB_DATABASE=/Users/ryan/Repos/ddm-spring-calculator/web/storage/db.sqlite

SHOPIFY_API_KEY=***
SHOPIFY_API_SECRET=***  

Any idea why I am getting the CSRF token issue?

I was able to solve this using the extremely helpful post here: https://rafaelcg.com/blog/developer/csrf-on-shopify-apps/ (Thanks Rafael!)

There is 1 caveat though. I got it working based on this general idea. Here is how I actually got it working:

import { useAppBridge } from "@shopify/app-bridge-react";
import { authenticatedFetch } from "@shopify/app-bridge/utilities";

const MyComponent: React.FC = () => {
    const fetch = authenticatedFetch(useAppBridge());
    const csrf = useCsrf();

    const getMyStuff = async () => {
        const response = await fetch('/api/stuff?shop=quickstart-f783802a.myshopify.com', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-TOKEN': await csrf()
            },
        });

        if (!response.ok) {
            setResultText("Rats and shucks!");
            throw new Error(`HTTP error! Status: ${response.status}`);
        } else {
            setResultText("Oh snap, it worked!");
        }
    };