Fixed: CORS errors from admin UI extension making requests to app backend

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 :).

1 Like

Hi Alex,

Thanks for sharing this - I’ll also let the community here chime in if there’s a better experience.