shopify-api-js
shopify-api-js copied to clipboard
Webooks aren't responding while using bodyparser as middleware
Issue summary
With the Shopify Node App when I add the koa-bodyparser, the webhook (APP_UNINSTALLED) gets registered successfully but it doesn't respond when I uninstall the app.
Expected behavior
There shouldn't be any issue with the webhook while using the bodyparser.
Actual behavior
The webhook (app/uninstalled) doesn't respond while using the bodyparser.
Steps to reproduce the problem
- Download/ clone the Shopify Node App
- Install koa-bodyparser i.e.
npm i koa-bodyparser - Add these lines in server.js-
const bodyParser = require("koa-bodyparser");
server.use(bodyParser());
(sever variable contains the koa initialization)
Alternatively, you can copy the following code and replace the server.js
import "@babel/polyfill";
import dotenv from "dotenv";
import "isomorphic-fetch";
import createShopifyAuth, { verifyRequest } from "@shopify/koa-shopify-auth";
import Shopify, { ApiVersion } from "@shopify/shopify-api";
import Koa from "koa";
import next from "next";
import Router from "koa-router";
const bodyParser = require("koa-bodyparser");
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();
Shopify.Context.initialize({
API_KEY: process.env.SHOPIFY_API_KEY,
API_SECRET_KEY: process.env.SHOPIFY_API_SECRET,
SCOPES: process.env.SCOPES.split(","),
HOST_NAME: process.env.HOST.replace(/https:\/\//, ""),
API_VERSION: ApiVersion.October20,
IS_EMBEDDED_APP: true,
// This should be replaced with your preferred storage strategy
SESSION_STORAGE: new Shopify.Session.MemorySessionStorage(),
});
// Storing the currently active shops in memory will force them to re-login when your server restarts. You should
// persist this object in your app.
const ACTIVE_SHOPIFY_SHOPS = {};
app.prepare().then(async () => {
const server = new Koa();
const router = new Router();
server.keys = [Shopify.Context.API_SECRET_KEY];
server.use(
createShopifyAuth({
async afterAuth(ctx) {
// Access token and shop available in ctx.state.shopify
const { shop, accessToken, scope } = ctx.state.shopify;
const host = ctx.query.host;
ACTIVE_SHOPIFY_SHOPS[shop] = scope;
const response = await Shopify.Webhooks.Registry.register({
shop,
accessToken,
path: "/webhooks",
topic: "APP_UNINSTALLED",
webhookHandler: async (topic, shop, body) => {
// this function doesn't run
delete ACTIVE_SHOPIFY_SHOPS[shop];
},
});
if (!response.success) {
console.log(
`Failed to register APP_UNINSTALLED webhook: ${response.result}`
);
}
// Redirect to app with shop parameter upon auth
ctx.redirect(`/?shop=${shop}&host=${host}`);
},
})
);
server.use(bodyParser());
const handleRequest = async (ctx) => {
await handle(ctx.req, ctx.res);
ctx.respond = false;
ctx.res.statusCode = 200;
};
router.post("/webhooks", async (ctx) => {
try {
await Shopify.Webhooks.Registry.process(ctx.req, ctx.res);
console.log(`Webhook processed, returned status code 200`);
} catch (error) {
console.log(`Failed to process webhook: ${error}`);
}
});
router.post(
"/graphql",
verifyRequest({ returnHeader: true }),
async (ctx, next) => {
await Shopify.Utils.graphqlProxy(ctx.req, ctx.res);
}
);
router.get("(/_next/static/.*)", handleRequest); // Static content is clear
router.get("/_next/webpack-hmr", handleRequest); // Webpack content is clear
router.get("(.*)", async (ctx) => {
const shop = ctx.query.shop;
// This shop hasn't been seen yet, go through OAuth to create a session
if (ACTIVE_SHOPIFY_SHOPS[shop] === undefined) {
ctx.redirect(`/auth?shop=${shop}`);
} else {
await handleRequest(ctx);
}
});
server.use(router.allowedMethods());
server.use(router.routes());
server.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`);
});
});
- Run the app with Shopify Cli i.e.
shopify node serve
I have been dealing with the same issue all night. It has to do with how the new shopify node api processes webhooks, namely the Shopify.Webhooks.Registry.process(ctx.req, ctx.res) method.
Hopefully this reply from the staff points you in the right direction.
As a fun aside, it appears that using koa-bodyparser or otherwise modifying ctx.req or ctx.request in any way (which I was trying to do in order to get the raw request body in order to verify webhooks) seems to break the new Shopify GraphQL api too (Shopify.Clients.Graphql(shop, accessToken) that is). Commenting out my koa-bodyparser call made it work again.
Stumbled on this as well. What I did is disabled bodyparser on the webhooks route and it worked.
Perhaps try this before your bodyParser()?
E.g.
server.use('/webhooks', bodyParser.raw({ type: 'application/json' }));
server.use(bodyParser());
Source: https://gist.github.com/scottdixon/3d8ea3ab939f5935b486951d63aebd6d
In case someone else is struggling with this and hasn't used a body parser, try commenting out all the other things no matter how trivial they might be. I hadn't used the bodyparser myself and had the same issue, but it turned out that another package which I was relying on (routing-controllers) simply added the bodyparser instead of me. Silly, I know, but maybe this comment helps someone as it would have helped me.
Stumbled on this as well. What I did is disabled bodyparser on the webhooks route and it worked.
Perhaps try this before your bodyParser()?
E.g.
server.use('/webhooks', bodyParser.raw({ type: 'application/json' }));server.use(bodyParser());Source: https://gist.github.com/scottdixon/3d8ea3ab939f5935b486951d63aebd6d
@litkod Any idea how this works with koa-bodyparser?
Still, this issue exists.
I found this solution for now.
server.use(async (ctx, next) => {
if (ctx.path === '/webhooks') ctx.disableBodyParser = true;
await next();
});
server.use(bodyParser());
For what it's worth: this happens with express.json as well, not just koa-bodyparser.
Workaround is really just to make sure that the body-parsing middleware does not run for the webhook route; the underlying cause of the issue is that Webhooks.Registry.process relies on the req.on('end') event, and body-parsing middleware consumes this event, meaning process() is effectively blocked from running, since once the event has been consumed by the middleware, it's over.
Documentation regarding body parsers here
Note that webhook processing in v6.0.0 will be changing (along with just about everything else!) ... release candidate v6.0.0-rc1 is available.