I have a basic remix app, using the standard setup from the example app. Yesterday, all of my webhook requests started failing validation. I’m using the helper function for authentication. This is the code of one webhook:
import type { ActionFunctionArgs } from "@remix-run/node";
import { authenticate } from "../shopify.server";
export const action = async ({ request }: ActionFunctionArgs) => {
const { shop, session, topic } = await authenticate.webhook(request);
console.log(`Received ${topic} webhook for ${shop}`);
if (session) {
// hanlde topic
}
return new Response();
};
This is a classic issue when handling Shopify webhooks in Node.js. The “missing hmac” error is misleading; the real problem is that your HMAC validation is failing because a body-parser middleware (like express.json()) is altering the request body before your verification function can access it.
Shopify’s validation requires the raw, unparsed request body to generate a matching signature, but by default, Express modifies req.body into a parsed JSON object first, causing the mismatch.
The correct solution is to capture the raw body before it gets parsed. You can do this by adding a verify function to your express.json() middleware, which saves the raw buffer to a new property like req.rawBody. Your validation function must then use this raw body for the HMAC comparison. Only after a successful verification should you parse the body for your own use.
You can try this out:
const express = require('express');
const crypto = require('crypto');
const app = express();
// A function to verify the webhook using the raw body
const verifyWebhook = (req, res, next) => {
const hmac = req.get('X- Shopify-Hmac-Sha256');
const body = req.rawBody; // Use the rawBody, not req.body
const shopifySecret = 'YOUR_SHOPIFY_APP_SECRET'; // Replace with your secret
const generatedHash = crypto
.createHmac('sha256', shopifySecret)
.update(body, 'utf8')
.digest('base64');
if (crypto.timingSafeEqual(Buffer.from(generatedHash), Buffer.from(hmac))) {
req.body = JSON.parse(body); // Now parse the body for your own use
next();
} else {
res.status(401).send('Could not verify webhook');
}
};
// Configure express.json() to save the raw body before parsing
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf;
}
}));
// Your webhook route, with the verification middleware
app.post('/webhooks/orders/create', verifyWebhook, (req, res) => {
// If we get here, the webhook is verified.
console.log('🎉 Webhook received and verified:', req.body);
res.status(200).send('OK');
});
app.listen(3000, () => console.log('Server is listening on port 3000'));
This code correctly sets up the middleware, uses req.rawBody for the critical validation step, and then makes the parsed JSON available to your final route handler.
Thanks for your response. As you can see from my description, I’m not using Express. I’m using the basic, out of the box Remix app, and don’t have any middleware set up. It had been working without a problem. This is the webhook:
import type { ActionFunctionArgs } from "@remix-run/node";
import { authenticate } from "../shopify.server";
export const action = async ({ request }: ActionFunctionArgs) => {
const { shop, session, topic } = await authenticate.webhook(request);
console.log(`Received ${topic} webhook for ${shop}`);
if (session) {
// hanlde topic
}
return new Response();
};