Webhooks dont work when I run locally, but works when I run with Shopify CLI

egillanton
Shopify Partner
11 2 2

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:

  1. Run server: npm run dev or npm start
  2. Setup ngrok: ngrok http 8081 
  3. Update the App Setup URLs accordingly on the partner App page.  
  4. Install the App to the development store.

What happens:

  • All the actions done on the index page are successful without any noticeable errors on the server-side nor the client-side. 
  • Webhooks for ORDERS_PAID and ORDERS_FULLFILLED get registered. 
  • When an order gets paid or fulfilled, neither of the webhook handlers receive any data.
  • https://my-store.myshopify.com/admin/api/2020-10/webhooks.json returns an empty list: 

 

 

{
"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}`);
  });
});

 

 

Reply 1 (1)

egillanton
Shopify Partner
11 2 2

When I try to manually test the webhook, I get 403 Forbidden Error.

 

What I did:

  1. Run server: npm run dev or npm start
  2. Setup ngrok: ngrok http 8081 
  3. Update the App Setup URLs accordingly on the partner App page.  
  4. Install the App to the development store.
  5. Navigate to https://my-store.myshopify.com/admin/settings/notifications
  6. Create a Webhook for order payment the calls: https://my-ngrok-url.io/webhooks/orders/paid
  7. Send test notification

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