After a more or less lovely experience building an app block extension in development mode, I discovered that the deployed version couldn’t make a simple GET request to my app backend. I was seeing CORS errors in the browser like this:
Access to fetch at
'https://dev.my-app.com/api/mappings/48053024850153,48053024882921'
from origin 'https://extensions.shopifycdn.com' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
Redirect is not allowed for a preflight request.
Our app backend is node/typescript, based on the node template from Shopify. Some digging in the TLDR basement of Shopify’s docs uncovered some discussion about how extensions run in a web worker and need specific CORS headers from the server. After a bunch of fiddling, I also figured out that Shopify’s validateAuthenticatedSession()
middleware was causing the CORS preflight OPTIONS
requests to be redirected, which is no bueno.
I finally got it to work by moving the route handler for my extension’s request to before I mount the shopify.validateAuthenticatedSession()
middleware so the middleware isn’t running on that route. I also had to use the cors
middleware (npm install cors
) to coax CORS to play nice. Unfortunately, no validateAuthenticatedSession
middleware means there’s no session available in the route handler, so I found some helpful tips on decoding the access token to extract the shop domain (or logged in customer_id
if you want). Here’s the relevant bits:
import cors from "cors";
//...
const uiExtensionCorsOpts = {
origin: "https://extensions.shopifycdn.com",
methods: "GET,OPTIONS",
allowedHeaders: "Content-Type,Authorization",
};
app.options("/api/mappings/:variantIds", cors(uiExtensionCorsOpts));
app.get("/api/mappings/:variantIds", cors(uiExtensionCorsOpts), async (req, res) => {
// Because we can't use the session we need to extract the domain from the access token
// https://shopify.dev/docs/api/customer-account-ui-extensions/2025-01/apis/session-token
const authHeader = req.headers['authorization'];
// Extract JWT payload, adding padding to make valid base64
const jwtPayload = authHeader?.split('.')[1] + '==';
const jwtClaims = JSON.parse(Buffer.from(jwtPayload, 'base64').toString('utf8'));
const shopDomain = jwtClaims['dest'].replace('https://', '');
const variantIds = req.params.variantIds;
// ... etc.
});
// Validate active session (needs to be AFTER the extension request handler)
app.use('/api/*', shopify.validateAuthenticatedSession());
// ... other route handlers that need a session
I hope that helps somebody. Let me know if I’m doing it the hard way :).