Topics covering webhook creation & management, event handling, Pub/Sub, and Eventbridge, in Shopify apps.
Problem:
I am developing a Node / Next App (created using Shopify CLI)
When I run the command shopify serve everything works perfectly on my local machine and also all the webhooks respond.
When I do:
What happens:
{
"webhooks": []
}
Has anyone had the same problem before? and how did you resolve this error?
package.json
{
"name": "shopify-node-app",
"version": "1.0.0",
"description": "Shopify's node app for CLI tool",
"scripts": {
"test": "jest",
"dev": "cross-env NODE_ENV=development nodemon ./server/index.js --watch ./server/index.js",
"build": "next build",
"start": "cross-env NODE_ENV=production node ./server/index.js"
},
...
}
server/index.js
require('@babel/register')({
presets: ['@babel/preset-env'],
ignore: ['node_modules']
});
module.exports = require('./server.js');
server/server.js
import "@babel/polyfill";
import dotenv from "dotenv";
import "isomorphic-fetch";
import createShopifyAuth, { verifyRequest } from "@shopify/koa-shopify-auth";
import graphQLProxy, { ApiVersion } from "@shopify/koa-shopify-graphql-proxy";
import Koa from "koa";
import koaBody from "koa-body";
import next from "next";
import Router from "koa-router";
import session from "koa-session";
import compose from "koa-compose";
import * as handlers from "./handlers/index";
import { receiveWebhook } from "@shopify/koa-shopify-webhooks";
import { orderPaid } from "../webhooks/orders/paid";
import { orderFulfilled } from "../webhooks/orders/fulfilled";
import { customerDataRequest } from "../webhooks/customers/data-request";
import { customerRedact } from "../webhooks/customers/redact";
import { shopRedact } from "../webhooks/shops/redact";
import {
getStoreData,
createStoreData,
deleteStoreData,
updateStoreData,
} from "../api/stores/store";
dotenv.config();
const port = parseInt(process.env.PORT, 10) || 8081;
const dev = process.env.NODE_ENV !== "production";
const app = next({
dev,
});
const handle = app.getRequestHandler();
const { SHOPIFY_API_SECRET, SHOPIFY_API_KEY, SCOPES } = process.env;
app.prepare().then(() => {
const server = new Koa();
const router = new Router();
const webhook = receiveWebhook({
secret: SHOPIFY_API_SECRET,
});
server.use(
session(
{
sameSite: "none",
secure: true,
},
server
)
);
server.keys = [SHOPIFY_API_SECRET];
server.use(
createShopifyAuth({
apiKey: SHOPIFY_API_KEY,
secret: SHOPIFY_API_SECRET,
scopes: [SCOPES],
async afterAuth(ctx) {
//Auth token and shop available in session
//Redirect to shop upon auth
const { shop, accessToken } = ctx.session;
await handlers.registerWebhooks(
shop,
accessToken,
"ORDERS_FULFILLED",
"/webhooks/orders/fulfilled",
ApiVersion.October19
);
await handlers.registerWebhooks(
shop,
accessToken,
"ORDERS_PAID",
"/webhooks/orders/paid",
ApiVersion.October19
);
ctx.cookies.set("shopOrigin", shop, {
httpOnly: false,
secure: true,
sameSite: "none",
});
ctx.redirect("/");
},
})
);
server.use(
graphQLProxy({
version: ApiVersion.October19,
})
);
router.post("/webhooks/orders/paid", webhook, (ctx) => {
console.log("received webhook: ", ctx.state.webhook.topic);
orderPaid(ctx.state.webhook)
.then(() => {
ctx.res.statusCode = 200;
})
.catch((err) => {
console.log(err);
ctx.res.statusCode = 500; // Internal server error
});
});
router.post("/webhooks/orders/fulfilled", webhook, (ctx) => {
console.log("received webhook: ", ctx.state.webhook.topic);
orderFulfilled(ctx.state.webhook)
.then(() => {
ctx.res.statusCode = 200;
})
.catch((err) => {
console.log(err);
ctx.res.statusCode = 500; // Internal server error
});
});
// GDPR mandatory webhooks
// Customer data request endpoint
router.post("/webhooks/customers/data_request", webhook, (ctx) => {
console.log("received webhook: ", ctx.state.webhook.topic);
customerDataRequest(ctx.state.webhook);
ctx.res.statusCode = 200;
});
// Customer data erasure endpoint
router.post("/webhooks/customers/redact", webhook, (ctx) => {
console.log("received webhook: ", ctx.state.webhook.topic);
customerRedact(ctx.state.webhook);
ctx.res.statusCode = 200;
});
// Shop data erasure endpoint
router.post("/webhooks/shops/redact", webhook, (ctx) => {
console.log("received webhook: ", ctx.state.webhook.topic);
shopRedact(ctx.state.webhook);
ctx.res.statusCode = 200;
});
router.get("/api/stores/:store", verifyRequest(), async (ctx) => {
await getStoreData(ctx)
.then((storeData) => {
console.log("");
console.log("storeData returned to route in server");
ctx.body =
storeData == null ? {} : JSON.parse(JSON.stringify(storeData));
ctx.set("Content-Type", "application/json");
ctx.respond = true;
ctx.res.statusCode = 200;
})
.catch((err) => {
ctx.res.statusCode = 500;
console.error(err);
});
});
router.post("/api/stores/:store", compose([koaBody(), verifyRequest()]), async (ctx) => {
await createStoreData(ctx)
.then((storeData) => {
ctx.body = JSON.parse(JSON.stringify(storeData));
ctx.respond = true;
ctx.res.statusCode = 200;
})
.catch((err) => {
ctx.res.statusCode = 500;
console.log(err);
});
});
router.put("/api/stores/:store", compose([koaBody(), verifyRequest()]), async (ctx) => {
await updateStoreData(ctx)
.then(() => {
ctx.respond = true;
ctx.res.statusCode = 200;
})
.catch((err) => {
ctx.res.statusCode = 500;
console.log(err);
});
});
router.delete("/api/stores/:store", verifyRequest(), async (ctx) => {
await deleteStoreData(ctx)
.then(() => {
ctx.res.statusCode = 202;
})
.catch((err) => {
ctx.res.statusCode = 500;
console.log(err);
});
});
router.get("/", verifyRequest(), async (ctx) => {
await app.render(ctx.req, ctx.res, "/index", ctx);
ctx.respond = true;
ctx.res.statusCode = 200;
});
router.get("(.*)", verifyRequest(), async ctx => {
await handle(ctx.req, ctx.res);
ctx.respond = false;
ctx.res.statusCode = 200;
});
server.use(router.allowedMethods());
server.use(router.routes());
server.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`);
});
});
When I try to manually test the webhook, I get 403 Forbidden Error.
What I did:
What I get:
When I open up my terminal that has my ngrok instance running it shows the following output when the test notification is sent:
ngrok by @inconshreveable (Ctrl+C to quit)
Session Status online
Account my-ngrok-account (Plan: Free)
Version 2.3.35
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding http://my-ngrok-url.io -> http://localhost:8081
Forwarding https://my-ngrok-url.io -> http://localhost:8081
Connections ttl opn rt1 rt5 p50 p90
139 2 0.03 0.03 5.43 56.60
HTTP Requests
POST /webhooks/orders/paid 403 Forbidden
POST /webhooks/orders/paid 403 Forbidden