Questions and discussions about using the Shopify CLI and Shopify-built libraries.
Introduction:
I am new to using Node. I have followed the Guide: https://shopify.dev/tutorials/build-a-shopify-app-with-node-and-react
I used the Shopify CLI to create a Node app
I am having problems calling my own API route from my index.js page. I am making a get request to an URL similar to https://123456789.ngrok.io/api/stores/my-development-shop.myshopify.com
I get the following response: Error: Request failed with status code 400 - Bad Request due to somehow my client iframe page that is rendered by index.js has not authenticated.
How do I authenticate my page so I can call my api?
Note: I am able to retrieve the data by typing in my authenticated browser window:
https://my-develop-store.myshopify.com/admin/apps/APP-ID/api/stores/my-develop-store.myshopify.com
I even tried adding the Cookies from the ctx into the header of my request.
Error:
Error: Request failed with status code 400
...
{
config: {
url: 'https://123456789.ngrok.io/api/stores/my-develop-store.myshopify.com',
method: 'get',
headers: {
Accept: 'application/json',
Cookies: 'shopifyNonce=160078696775500;shopifyNonce.sig=5FoocFlgMKZ2YrgmhBqgSN-o0Eg;shopOrigin=my-develop-store.myshopify.com;shopOrigin.sig=6reeQR8RvnjRchwYFuG-kOl480Q;koa:sess=eyJzaG9wIjoicGF5ZGF5LWRldmVsb3AubXlzaG9waWZ5LmNvbSIsImFjY2Vzc1Rva2VuIjoic2hwYXRfOTI1YjExMjdlNWY1NTU3ODNhNWU5N2M5MDJhYjcwNTAiLCJfZXhwaXJlIjoxNjAwODczMzcwMDY0LCJfbWF4QWdlIjo4NjQwMDAwMH0=;koa:sess.sig=1d2AeB5GMmoEBdwD5bB_0MHU8HM',
'User-Agent': 'axios/0.20.0'
},
transformRequest: [ [Function: transformRequest] ],
transformResponse: [ [Function: transformResponse] ],
timeout: 0,
adapter: [Function: httpAdapter],
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
maxBodyLength: -1,
validateStatus: [Function: validateStatus],
data: undefined
},
request: <ref *1> ClientRequest {
_events: [Object: null prototype] {
socket: [Function (anonymous)],
abort: [Function (anonymous)],
aborted: [Function (anonymous)],
connect: [Function (anonymous)],
error: [Function (anonymous)],
timeout: [Function (anonymous)],
prefinish: [Function: requestOnPrefinish]
},
_eventsCount: 7,
_maxListeners: undefined,
outputData: [],
outputSize: 0,
writable: true,
destroyed: false,
_last: true,
chunkedEncoding: false,
shouldKeepAlive: false,
useChunkedEncodingByDefault: false,
sendDate: false,
_removedConnection: false,
_removedContLen: false,
_removedTE: false,
_contentLength: 0,
_hasBody: true,
_trailer: '',
finished: true,
_headerSent: true,
socket: TLSSocket {
_tlsOptions: [Object],
_secureEstablished: true,
_securePending: false,
_newSessionPending: false,
_controlReleased: true,
secureConnecting: false,
_SNICallback: null,
servername: '123456789.ngrok.io',
alpnProtocol: false,
authorized: true,
authorizationError: null,
encrypted: true,
_events: [Object: null prototype],
_eventsCount: 10,
connecting: false,
_hadError: false,
_parent: null,
_host: '123456789.ngrok.io',
_readableState: [ReadableState],
_maxListeners: undefined,
_writableState: [WritableState],
allowHalfOpen: false,
_sockname: null,
_pendingData: null,
_pendingEncoding: '',
server: undefined,
_server: null,
ssl: [TLSWrap],
_requestCert: true,
_rejectUnauthorized: true,
parser: null,
_httpMessage: [Circular *1],
[Symbol(res)]: [TLSWrap],
[Symbol(verified)]: true,
[Symbol(pendingSession)]: null,
[Symbol(async_id_symbol)]: 135897,
[Symbol(kHandle)]: [TLSWrap],
[Symbol(kSetNoDelay)]: false,
[Symbol(lastWriteQueueSize)]: 0,
[Symbol(timeout)]: null,
[Symbol(kBuffer)]: null,
[Symbol(kBufferCb)]: null,
[Symbol(kBufferGen)]: null,
[Symbol(kCapture)]: false,
[Symbol(kBytesRead)]: 0,
[Symbol(kBytesWritten)]: 0,
[Symbol(connect-options)]: [Object]
},
_header: 'GET /auth HTTP/1.1\r\n' +
'Accept: application/json\r\n' +
'Cookies: shopifyNonce=160078696775500;shopifyNonce.sig=5FoocFlgMKZ2YrgmhBqgSN-o0Eg;shopOrigin=my-develop-store.myshopify.com;shopOrigin.sig=6reeQR8RvnjRchwYFuG-kOl480Q;koa:sess=eyJzaG9wIjoicGF5ZGF5LWRldmVsb3AubXlzaG9waWZ5LmNvbSIsImFjY2Vzc1Rva2VuIjoic2hwYXRfOTI1YjExMjdlNWY1NTU3ODNhNWU5N2M5MDJhYjcwNTAiLCJfZXhwaXJlIjoxNjAwODczMzcwMDY0LCJfbWF4QWdlIjo4NjQwMDAwMH0=;koa:sess.sig=1d2AeB5GMmoEBdwD5bB_0MHU8HM\r\n' +
'User-Agent: axios/0.20.0\r\n' +
'Host: 123456789.ngrok.io\r\n' +
'Connection: close\r\n' +
'\r\n',
_onPendingData: [Function: noopPendingOutput],
agent: Agent {
_events: [Object: null prototype],
_eventsCount: 2,
_maxListeners: undefined,
defaultPort: 443,
protocol: 'https:',
options: [Object],
requests: {},
sockets: [Object],
freeSockets: {},
keepAliveMsecs: 1000,
keepAlive: false,
maxSockets: Infinity,
maxFreeSockets: 256,
scheduling: 'fifo',
maxTotalSockets: Infinity,
totalSocketCount: 1,
maxCachedSessions: 100,
_sessionCache: [Object],
[Symbol(kCapture)]: false
},
socketPath: undefined,
method: 'GET',
maxHeaderSize: undefined,
insecureHTTPParser: undefined,
path: '/auth',
_ended: true,
res: IncomingMessage {
_readableState: [ReadableState],
_events: [Object: null prototype],
_eventsCount: 3,
_maxListeners: undefined,
socket: [TLSSocket],
httpVersionMajor: 1,
httpVersionMinor: 1,
httpVersion: '1.1',
complete: true,
headers: [Object],
rawHeaders: [Array],
trailers: {},
rawTrailers: [],
aborted: false,
upgrade: false,
url: '',
method: null,
statusCode: 400,
statusMessage: 'Bad Request',
client: [TLSSocket],
_consuming: false,
_dumped: false,
req: [Circular *1],
responseUrl: 'https://123456789.ngrok.io/auth',
redirects: [],
[Symbol(kCapture)]: false
},
aborted: false,
timeoutCb: null,
upgradeOrConnect: false,
parser: null,
maxHeadersCount: null,
reusedSocket: false,
host: '123456789.ngrok.io',
protocol: 'https:',
_redirectable: Writable {
_writableState: [WritableState],
_events: [Object: null prototype],
_eventsCount: 2,
_maxListeners: undefined,
_options: [Object],
_ended: true,
_ending: true,
_redirectCount: 1,
_redirects: [],
_requestBodyLength: 0,
_requestBodyBuffers: [],
_onNativeResponse: [Function (anonymous)],
_currentRequest: [Circular *1],
_currentUrl: 'https://123456789.ngrok.io/auth',
_isRedirect: true,
[Symbol(kCapture)]: false
},
[Symbol(kCapture)]: false,
[Symbol(kNeedDrain)]: false,
[Symbol(corked)]: 0,
[Symbol(kOutHeaders)]: [Object: null prototype] {
accept: [Array],
cookies: [Array],
'user-agent': [Array],
host: [Array]
}
},
response: {
status: 400,
statusText: 'Bad Request',
headers: {
'content-type': 'text/plain; charset=utf-8',
'content-length': '37',
date: 'Tue, 22 Sep 2020 15:03:10 GMT',
connection: 'close'
},
config: {
url: 'https://123456789.ngrok.io/api/stores/my-develop-store.myshopify.com',
method: 'get',
headers: [Object],
transformRequest: [Array],
transformResponse: [Array],
timeout: 0,
adapter: [Function: httpAdapter],
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
maxBodyLength: -1,
validateStatus: [Function: validateStatus],
data: undefined
},
request: <ref *1> ClientRequest {
_events: [Object: null prototype],
_eventsCount: 7,
_maxListeners: undefined,
outputData: [],
outputSize: 0,
writable: true,
destroyed: false,
_last: true,
chunkedEncoding: false,
shouldKeepAlive: false,
useChunkedEncodingByDefault: false,
sendDate: false,
_removedConnection: false,
_removedContLen: false,
_removedTE: false,
_contentLength: 0,
_hasBody: true,
_trailer: '',
finished: true,
_headerSent: true,
socket: [TLSSocket],
_header: 'GET /auth HTTP/1.1\r\n' +
'Accept: application/json\r\n' +
'Cookies: shopifyNonce=160078696775500;shopifyNonce.sig=5FoocFlgMKZ2YrgmhBqgSN-o0Eg;shopOrigin=my-develop-store.myshopify.com;shopOrigin.sig=6reeQR8RvnjRchwYFuG-kOl480Q;koa:sess=eyJzaG9wIjoicGF5ZGF5LWRldmVsb3AubXlzaG9waWZ5LmNvbSIsImFjY2Vzc1Rva2VuIjoic2hwYXRfOTI1YjExMjdlNWY1NTU3ODNhNWU5N2M5MDJhYjcwNTAiLCJfZXhwaXJlIjoxNjAwODczMzcwMDY0LCJfbWF4QWdlIjo4NjQwMDAwMH0=;koa:sess.sig=1d2AeB5GMmoEBdwD5bB_0MHU8HM\r\n' +
'User-Agent: axios/0.20.0\r\n' +
'Host: 123456789.ngrok.io\r\n' +
'Connection: close\r\n' +
'\r\n',
_onPendingData: [Function: noopPendingOutput],
agent: [Agent],
socketPath: undefined,
method: 'GET',
maxHeaderSize: undefined,
insecureHTTPParser: undefined,
path: '/auth',
_ended: true,
res: [IncomingMessage],
aborted: false,
timeoutCb: null,
upgradeOrConnect: false,
parser: null,
maxHeadersCount: null,
reusedSocket: false,
host: '123456789.ngrok.io',
protocol: 'https:',
_redirectable: [Writable],
[Symbol(kCapture)]: false,
[Symbol(kNeedDrain)]: false,
[Symbol(corked)]: 0,
[Symbol(kOutHeaders)]: [Object: null prototype]
},
data: 'Expected a valid shop query parameter'
},
isAxiosError: true,
toJSON: [Function: toJSON]
}
Files:
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 next from "next";
import Router from "koa-router";
import session from "koa-session";
import * as handlers from "./handlers/index";
import { receiveWebhook } from "@shopify/koa-shopify-webhooks";
import { orderPaid } from "../webhooks/orders/paid";
import { customerDataRequest } from "../webhooks/customers/data-request";
import { customerRedact } from "../webhooks/customers/redact";
import { shopRedact } from "../webhooks/shops/redact";
import { getStoreInfoById } from "../api/stores/store";
import fetch from "isomorphic-fetch";
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, HOST } = 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_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.get("/", verifyRequest(), async (ctx) => {
if (typeof ctx.query.shop !== "undefined") {
await app.render(ctx.req, ctx.res, "/index", ctx.query);
ctx.respond = true;
ctx.res.statusCode = 200;
}
});
router.get("/api/stores/:store", verifyRequest(), async (ctx) => {
// example: /api/stores/my-develop-store.myshopify.com
// Check if the request is made by the right store
console.log("Route called: /api/stores/:store");
const { shop, accessToken } = ctx.session;
if (ctx.params.store === shop) {
await getStoreInfoById(ctx)
.then((storeInfo) => {
if (typeof storeInfo !== "undefined") {
ctx.body = JSON.parse(JSON.stringify(storeInfo));
ctx.set('Content-Type', 'application/json');
ctx.respond = true;
ctx.res.statusCode = 200;
}
})
.catch((err) => {
ctx.res.statusCode = 500;
console.log(err);
});
} else {
ctx.res.statusCode = 403;
}
});
router.post("/webhooks/orders/paid", webhook, (ctx) => {
orderPaid(ctx.state.webhook);
ctx.res.statusCode = 200;
});
router.post("/webhooks/customers/data_request", webhook, (ctx) => {
customerDataRequest(ctx.state.webhook);
ctx.res.statusCode = 200;
});
router.post("/webhooks/customers/redact", webhook, (ctx) => {
customerRedact(ctx.state.webhook);
ctx.res.statusCode = 200;
});
router.post("/webhooks/shops/redact", webhook, (ctx) => {
shopRedact(ctx.state.webhook);
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}`);
});
});
pages/index.js
class HomePage extends React.Component {
// Prepares the props sent to the constructor
// Server sends the ctx.query as the parameter
static async getInitialProps(ctx_query) {
const host = ctx_query.req.headers.host;
const shop = ctx_query.query.shop;
// If executed on Server Side
if (typeof shop === "undefined" || typeof host === "undefined") {
return {};
}
// console.log("host:", host);
// console.log("shop:", shop);
// console.log(ctx_query);
let props = {
shop: shop,
};
// const url_query = ctx_query.asPath.substring(1);
console.log(url_query);
const url = "https://" + host + '/api/stores/' + shop;
console.log("Endpoint URL:", url);
console.log("");
axios.get(url, { headers: { 'Accept': 'application/json' } })
.then((response) => {
console.log("Response Successful")
if (typeof response.data !== "undefined") {
console.log("Data from response:", response.data);
}
})
.catch((error) => {
// handle error
console.log(error); // I always get an 400 Bad Request
})
.finally(() => {
// always executed
});
return props;
}
...
}
Solved! Go to the solution
This is an accepted solution.
Solution Found:
I had forgotten a crucial step, and that was to update the environment variable HOST used by the registerWebhooks method.
The reason I was receiving a 403 Error, was because it was receiving I guess the secret from the previous instance of the running app. Maybe someone can back me up on that.
import { registerWebhook } from "@shopify/koa-shopify-webhooks";
export const registerWebhooks = async (
shop,
accessToken,
type,
url,
apiVersion
) => {
const address = `${process.env.HOST}${url}`;
const registration = await registerWebhook({
address: address,
topic: type,
accessToken,
shop,
apiVersion,
});
if (registration.success) {
console.log(`Successfully registered webhook for ${type}!`);
console.log(`${address}`);
console.log("");
} else {
console.error("Failed to register webhook", registration.result.data.webhookSubscriptionCreate);
}
};
This is an accepted solution.
Solution Found:
I had forgotten a crucial step, and that was to update the environment variable HOST used by the registerWebhooks method.
The reason I was receiving a 403 Error, was because it was receiving I guess the secret from the previous instance of the running app. Maybe someone can back me up on that.
import { registerWebhook } from "@shopify/koa-shopify-webhooks";
export const registerWebhooks = async (
shop,
accessToken,
type,
url,
apiVersion
) => {
const address = `${process.env.HOST}${url}`;
const registration = await registerWebhook({
address: address,
topic: type,
accessToken,
shop,
apiVersion,
});
if (registration.success) {
console.log(`Successfully registered webhook for ${type}!`);
console.log(`${address}`);
console.log("");
} else {
console.error("Failed to register webhook", registration.result.data.webhookSubscriptionCreate);
}
};