What's your biggest current challenge? Have your say in Community Polls along the right column.
Our Partner & Developer boards on the community are moving to a brand new home: the .dev community forums! While you can still access past discussions here, for all your future app and storefront building questions, head over to the new forums.

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

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

egillanton
Shopify Partner
11 2 4

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 4

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