Re: CORS issue when requesting data from shopify app (remix template) during local theme development

Solved

CORS issue when requesting data from shopify app (remix template) during local theme development

Danh11
Shopify Partner
87 2 39

I am developing ontop of the Remix app template. I have a route setup to return user data from the database.

 

I am making a request via the shopify app proxy to a route setup to fetch data from the app.

I understand the shopify app proxy won't work while running the theme locally (I think?), so I have a domain pointing to a tunnel which points to the app running on localhost.

 

I can successfully make requests by hitting the endpoint via the tunnel in my browser.

 

Although the request is rejected when being made from the theme locally. I am returning headers to the preflight request, which I think are right, but it's still not working.

 

I'm wondering if some middlewear when running the theme locally is interfering with something.

 

api.get-user.jsx

export async function loader({ request }) {
  const corsHeaders = {
    "Access-Control-Allow-Origin": "http://127.0.0.1:9292",
    "Access-Control-Allow-Methods": "GET, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type",
  };

  if (request.method === 'OPTIONS') {
    return json({
      status: 200,
      headers: corsHeaders,
      body: ''
    });
  }
  
  return json({ body: 'data' });
}

JS in theme

async getUser() {    
    const url = `${this.appUrl}/api/get-user`;
  
    try {
      const response = await fetch(url, {
        method: 'GET',
        headers: { 'Content-Type': 'application/json' },
      });
  
      if (!response.ok) throw new Error('Network response error:', response.statusText);
  
      const user = await response.json();
  
      return user;
  
    } catch (error) {
      console.error('Error:', error);
    }
  }


I am aware that I should be checking the digital signature from the request, and the preflight response headers probably aren't the greatest, but I'm just trying to get this working first.

Accepted Solution (1)

Danh11
Shopify Partner
87 2 39

This is an accepted solution.

The way to get this working is to use the CORS function from the remix-utils package.

 

If installing this on the Remix template of the shopify app, you will need to modify the remix.config.js file by including serverDependenciesToBundle: [ /^remix-utils.*/ ] in the module.exports as documented here.  This is because remix-utils is published as ESM only and the remix.config.js file has the serverModuleFormat set to cjs. My editor still yells at me about the incorrect import method, but it works nonetheless. This is my updated remix.config.js file:

 

remix.config.js

 

if (
  process.env.HOST &&
  (!process.env.SHOPIFY_APP_URL ||
    process.env.SHOPIFY_APP_URL === process.env.HOST)
) {
  process.env.SHOPIFY_APP_URL = process.env.HOST;
  delete process.env.HOST;
}

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  ignoredRouteFiles: ["**/.*"],
  appDirectory: "app",
  serverModuleFormat: "cjs",
  future: {
    v2_errorBoundary: true,
    v2_headers: true,
    v2_meta: true,
    v2_normalizeFormMethod: true,
    v2_routeConvention: true,
    v2_dev: {
      port: process.env.HMR_SERVER_PORT || 8002,
    },
  },
  serverDependenciesToBundle: [
    /^remix-utils.*/,
  ],
};

 

Then, import cors from the remix-utils package and update the return in your loader function of your API route with the cors function, as per their docs linked above. It should look like the following:

 

api.get-user.jsx

 

import { json } from '@remix-run/node';
import { cors } from 'remix-utils/cors';

export async function loader({ request }) {

  const response = json({ body: 'data' });

  return await cors(request, response);
}

 

I am now successfully making requests from the theme which is running locally.

 

View solution in original post

Replies 29 (29)

Liam
Community Manager
3108 341 881

Hi Danh11,

 

From your code, it seems like you're handling the CORSflight request. However, you might want to add the "Access-Control-Allow-Credentials" header and set it to "true" if you're using cookies for authentication. Also, consider allowing all headers in theAccess-Control-Allow-Headers" in your CORS configuration for testing purposes:

const cors = {
  "Access-Control-Allow-Origin": "http://1270.0.1:9292",
  "Access-Control-Allow-Methods": "GET, OPTIONS",
  "Access-Control-Headers": "*",
  "Access-Control-Allow-Credentials": "true"
};

Moreover, ensure that your server is set up to handle OPTIONS requests. The preflight request is an OPTIONS request and server needs to respond with the appropriate CORS headers.

 

Hope this helps!

Liam | Developer Advocate @ Shopify 
 - Was my reply helpful? Click Like to let me know! 
 - Was your question answered? Mark it as an Accepted Solution
 - To learn more visit Shopify.dev or the Shopify Web Design and Development Blog

Danh11
Shopify Partner
87 2 39

Unfortunately the request is still being blocked. There were a couple of typos which I think were a mistake? By `Access-Control-Headers` I assume you meant `

Access-Control-Allow-Headers`? Fixing them didn't resolve the issue though.
 
As I'm using the Shopify app template, a lot of the server config is hidden and I can't find a way to access or modify it. All that I can see is `shopify.server.js`. I'm not able to find any info on how to modify the express instance which I think is running.
 
 

Danh11
Shopify Partner
87 2 39

This is an accepted solution.

The way to get this working is to use the CORS function from the remix-utils package.

 

If installing this on the Remix template of the shopify app, you will need to modify the remix.config.js file by including serverDependenciesToBundle: [ /^remix-utils.*/ ] in the module.exports as documented here.  This is because remix-utils is published as ESM only and the remix.config.js file has the serverModuleFormat set to cjs. My editor still yells at me about the incorrect import method, but it works nonetheless. This is my updated remix.config.js file:

 

remix.config.js

 

if (
  process.env.HOST &&
  (!process.env.SHOPIFY_APP_URL ||
    process.env.SHOPIFY_APP_URL === process.env.HOST)
) {
  process.env.SHOPIFY_APP_URL = process.env.HOST;
  delete process.env.HOST;
}

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  ignoredRouteFiles: ["**/.*"],
  appDirectory: "app",
  serverModuleFormat: "cjs",
  future: {
    v2_errorBoundary: true,
    v2_headers: true,
    v2_meta: true,
    v2_normalizeFormMethod: true,
    v2_routeConvention: true,
    v2_dev: {
      port: process.env.HMR_SERVER_PORT || 8002,
    },
  },
  serverDependenciesToBundle: [
    /^remix-utils.*/,
  ],
};

 

Then, import cors from the remix-utils package and update the return in your loader function of your API route with the cors function, as per their docs linked above. It should look like the following:

 

api.get-user.jsx

 

import { json } from '@remix-run/node';
import { cors } from 'remix-utils/cors';

export async function loader({ request }) {

  const response = json({ body: 'data' });

  return await cors(request, response);
}

 

I am now successfully making requests from the theme which is running locally.

 

Danh11
Shopify Partner
87 2 39

Just to note - This actually still does not work when making the request via the Shopify app proxy while on local development. It does work ok when the theme is deployed and running from the Shopify server.

 

Although making requests to the app URL (with the tunnel running feeding traffic to the app running locally) does work in local development. So at least this makes it possible to develop locally. The app URL can be swapped out for the Shopify app proxy when it comes to deployment. This won't be a great dev experience if/when the app proxy signature is checked on the endpoint provided by the app, but that's a problem for another time.

myvirtualteams
Shopify Partner
31 0 10

Hey @Danh11 We're also stuck at cors error just i'm not creating a theme .
I have created an app using shopify remix.

 

Created an route in the app which will open in browser rather than on the store admin app dashboard.

 

Updated the remix config files like this 

if (
  process.env.HOST &&
  (!process.env.SHOPIFY_APP_URL ||
    process.env.SHOPIFY_APP_URL === process.env.HOST)
) {
  process.env.SHOPIFY_APP_URL = process.env.HOST;
  delete process.env.HOST;
}

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  publicPath: process.env.SHOPIFY_APP_URL + "/build/",
  ignoredRouteFiles: ["**/.*"],
  appDirectory: "app",
  serverModuleFormat: "cjs",
  includeRoutes: [
    require.resolve("./app/routes/api.jsx"), // Include your API route
  ],
  future: {
    v2_errorBoundary: true,
    v2_headers: true,
    v2_meta: true,
    v2_normalizeFormMethod: true,
    v2_routeConvention: true,
    v2_dev: {
      port: process.env.HMR_SERVER_PORT || 8002,
    },
  },
};

As you can see I've give the public path with url where our app will be hosting I've named it SHOPIFY_APP_URL but it'll be heroku url where our app is hosted.

 

We've to do it as when we use as proxy with default publivPath it try to access build files using shopify domain and there is error not found.

We've add that route as app proxy so we can access through the store.

 

Then we've hosted our app on the heroku and added that url like this heroku_url/app_route in app proxy.

Now we're geting files but getting cors error while running that url as app aproxy.

 

css file is able to load but the build js files are giving cors errors.

 

I dont able to get where i will add cors headers so my store can access the shopify app build files hosted on heroku can remove cors errors

 

 

My Virtual Teams
Danh11
Shopify Partner
87 2 39

I'm not really familiar with what you're trying to do there, sorry!

 

If you're trying to define a new route, that would be done with a jsx or tsx file named appropriately. For example, api.endpoint.jsx for the path of /api/endpoint. You can then use the cors package that I mentioned in my previous comment to add the headers to the response.

 

Unless I have misinterpreted what you're trying to achieve?

seunoyebode
Shopify Partner
11 0 0

 

Hello @Danh11 

Thanks, i've been trying to make a fetch request from my extension to my server. Even tried what worked for you. My loader function is similar to yours. Any idea what I'm doing wrong?



fetch('https://insight-fog-gmc-implies.trycloudflare.com/app/make', {
  method: "POST",
  body: "Hello, World!",
  headers: {
    "Content-Type": "text/plain"
  }
})

 

Danh11
Shopify Partner
87 2 39

Hey! I need a little more information 🙂 What error are you getting? What does the function look like where you're making this request? What does your route look like? Is the route being hit by the request? Is it failing to return anything at all? 

seunoyebode
Shopify Partner
11 0 0

Thank you so much Danh11 for your response

this is the error I get from my browser when I trigger the fetch request

seunoyebode_0-1703941802838.png

I also believe the route is being hit as I get this error in my terminal 

seunoyebode_1-1703941901217.png

this is how my js in the liquid file looks like

seunoyebode_2-1703942020298.png

 

my remix route looks like this (code block), My primary aim here is to be able to send data from my theme app extension to my server 

import {
  Card,
  Page,
  Text,
  Layout,
  Divider,
  PageActions,
  BlockStack,
  RadioButton,
} from "@shopify/polaris";
import { authenticate } from "../shopify.server";
import { useState, useCallback} from 'react';
import { useSubmit, useNavigation } from "@remix-run/react";
import { redirect, json } from "@remix-run/node";
import { cors } from 'remix-utils/cors';

export const loader = async ({ request }) => {
  const { admin, session } =  await authenticate.admin(request);

   console.log(cors(request),"cors log");
   console.log(request,"request log");

   return null;


 

Danh11
Shopify Partner
87 2 39

It seems like the error is occuring during the preflight request that the client is making. The server isn't handling the OPTIONS request type, which is what the preflight is, so it's causing the error and in turn the client also fails on the preflight request.

 

It's a bit hard for me to tell you exactly how to fix it, but look into what a preflight request is and how to handle it. Maybe ChatGPT could point you in the right direction as well.

seunoyebode
Shopify Partner
11 0 0

Thanks for your response Dan.

I'm still on it 😅.

This time around. I'm hitting the proxy URL directly (image attached). However, it redirects to "https://store-url.com/auth/login" . I have the below in my loader function as seen on the Shopify Remix Template App Proxy Page

import { json } from "@remix-run/node";
import { authenticate } from "../shopify.server";
import { getMyAppModelData } from "~/db/model.server";
export const loader = async ({ request }) => {
  // Get the session for the shop that initiated the request to the app proxy.
  const { session } = await authenticate.public.appProxy(request);

  // Use the session data to make to queries to your database or additional requests.
  return json(await getMyAppModelData({shop: session.shop));
};



Do you know what the issue could be?

Thank you.


seunoyebode_0-1705891339390.png

 

techaccounts
Shopify Partner
2 0 0

Were you able to solve it, i am stuck at same point

seunoyebode
Shopify Partner
11 0 0

This YouTube video was helpful in properly setting up Shopify App Proxy. 

However, it didn’t help with properly getting back the data sent from the BE. 

I had to settle for the approach in this YouTube video

if you ever figure out a way to use App Proxy and also get the data sent from the BE in your liquid file, please share 

techaccounts
Shopify Partner
2 0 0

i can create product using app proxy but remix server doesn't respond with proper 200 status and not able to send json as well

attaboiaj
Shopify Partner
29 4 13

Did you try it by running by hosting urls somewhere
I hosted my app on a VPS and I am getting the cors error 

mehmettekn
Shopify Partner
10 0 3

Does anyone know how to implement this solution with the current version of the Shopify Remix app template now with Vite? There isn't a remix.config.js file anymore.

attaboiaj
Shopify Partner
29 4 13

You don't need to setup any Vite or remix configs


see my below code and just fixes your code similar to mine

 

Issue is: 
When you call Remix APIs Shopify send the OPTIONS requests so we need to CORS safe them and that's exactly what I did with my code 

https://community.shopify.com/c/online-store-and-theme/cors-issue-when-requesting-data-from-shopify-...

Ekomi-Dev
Shopify Partner
5 0 0

I am also currently getting a CORS error, but I am not building an app or theme, but an extension instead. 
I don't have remix.config.js file instead a vite.config.ts file that looks like this:

import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig, type UserConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

// Replace the HOST env var with SHOPIFY_APP_URL so that it doesn't break the remix server. The CLI will eventually
// stop passing in HOST, so we can remove this workaround after the next major release.
if (
process.env.HOST &&
(!process.env.SHOPIFY_APP_URL ||
process.env.SHOPIFY_APP_URL === process.env.HOST)
) {
process.env.SHOPIFY_APP_URL = process.env.HOST;
delete process.env.HOST;
}

const host = new URL(process.env.SHOPIFY_APP_URL || "http://localhost")
.hostname;

let hmrConfig;
if (host === "localhost") {
hmrConfig = {
protocol: "ws",
host: "localhost",
port: 64999,
clientPort: 64999,
};
} else {
hmrConfig = {
protocol: "wss",
host: host,
port: parseInt(process.env.FRONTEND_PORT!) || 8002,
clientPort: 443,
};
}

export default defineConfig({
server: {
port: Number(process.env.PORT || 3000),
hmr: hmrConfig,
fs: {
allow: ["app", "node_modules"],
},
},
plugins: [
remix({
ignoredRouteFiles: ["**/.*"],
}),
tsconfigPaths(),
],
build: {
assetsInlineLimit: 0,
},
}) satisfies UserConfig;
so I don't know how it will look like with you changes that you have made to make it work. 



attaboiaj
Shopify Partner
29 4 13

Ok if you are new to this I wasted a day to fix this error 

this error comes when you do POST/GET request to your remix API from your DEV store via embeds 

Reason is you are not managing your OPTIONS REQUEST correctly 

NOTE: you never gets this error when you using this "Cloudflare tunnel Urls" You gets these Cors errors when you host you host your Remix somewhere like a VPS

and with remix this is how you should you mange it

remix-utils is the main package here
check it's doc for how to restrict your site more.

 

import { json } from "@remix-run/node";
import { cors } from "remix-utils/cors";
import type { LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node";


export async function loader({ request }: LoaderFunctionArgs) {
  const url = new URL(request.url);
  const shop = url.searchParams.get("shop");
  
  // This is where you need to cors safe your request
  if (request.method === "OPTIONS") {
    const response = json({
      status: 200,
    });
    return await cors(request, response);
  }

  if (!shop) {
    return json({
      message: "Missing data. Required data: shop",
      method: "GET",
    });
  }

    const response = json({
    ok: true,
    message: "Success",
    data: "analyticDbResponse",
  });

  return cors(request, response);
}

const addAnalytic = async (data) => {
 return "some db stuff"
};

export async function action({ request }: ActionFunctionArgs) {

  // for double check I added this here too
  if (request.method === "OPTIONS") {
    const response = json({
      status: 200,
    });
    return await cors(request, response);
  }

  if (request.method == "POST") {
    let data = await request.json();
    try {
      if (data.shop !== undefined) {
        addAnalytic(data); // me doing some db updates
        const response = json({
          message: "success",
        });

        await cors(request, response);
        return response;
      } else {
        return json({
          message: "Missing data. Required data: shop",
          method: "POST",
        });
      }
    } catch (error) {
      console.log("error", error);
      return json(
        {
          message: "error with endpoint",
          method: "POST",
        },
        400,
      );
    }
  }
}

 



mehmettekn
Shopify Partner
10 0 3

This worked for me as well. I also wasted a couple days on this even after finding this post. I didn't realize the OPTIONS method is handled by the loader function.

 

Thanks @attaboiaj 

stefanb1234
Shopify Partner
13 0 4

Unfortunately I tried doing the same and I get some strange errors at a different level than in my loader/action methods:

ErrorResponseImpl {
status: 405,
statusText: 'Method Not Allowed',
internal: true,
data: 'Error: Invalid request method "OPTIONS"',
error: Error: Invalid request method "OPTIONS"
at getInternalRouterError (app/node_modules/@remix-run/router/router.ts:5402:5

 

This is my loader function:

export const loader = async ({ request }: LoaderFunctionArgs) => {
const { session, admin } = await authenticate.admin(request);

if (request.method === "OPTIONS") {
return await cors(request, json(null));
}

// some logic


return await cors(request, json(data));

}

 

Is there anything I skipped?

attaboiaj
Shopify Partner
29 4 13

Need more info

Are you using the latest packages
this code works out of the box the inital code that `shopify app init` comes with

Try to make a new base app and try my solutions

stefanb1234
Shopify Partner
13 0 4

It is working for me as well, I made a rookie mistake where I was sending requests to the wrong url

attaboiaj
Shopify Partner
29 4 13

Haha this response gonna saves a day for everyone. 

it's super weird that how things works here. 

Glad my solution helped

Yam
Shopify Partner
3 0 0

This error is really frustrating, so thank you for posting this! It helped me overcome one issue, but I'm left with another hurdle.

 

In my route file (api.proxy.js, shown below) I need to run a graphql query. The request will originate from my client-side Checkout.jsx file. I think the graphql requires that I have access to the admin api in my server side code. Any idea on how to do that in this scenario? I'm running in a local dev environment that I set up with the shopify cli (using remix). I've tried going through an app proxy and calling api.proxy.js directly from the front end of my app with a fetch command, but nothing works.

 

When I use an app proxy in my client-side code:

 

fetch(https://${shop}/apps/proxy)

 

, I still get a cors error. But maybe this is due to running it in a dev environment using a "...trycloudflare.com" tunnel? When I go to the proxy url (pasting url directly in a browser) that I set up in the partner portal, it successfully ends up at my api endpoint, but the admin client cannot be authenticated, which I think makes sense since I can't send a token or other authentication data when going to the url in a browser.

 

So, for testing, I am bypassing the proxy by going directly to my api endpoint (APPURL is pointing to the temp cloudflare url for my app):

 

const response = await fetch(`${APPURL}/api/proxy`, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${sessionToken}`,
      },
});

 

When I use authenticate.admin(request) in api.proxy.js, the above fetch produces this error: "code: 'ERR_INVALID_URL', input: 'xxxx.xxxx.myshopify.com'". Note that the "input" value is my store domain, but is not prefixed with "https://". Not sure if that matters.

Since that didn't work, I tried unauthenticated.admin(request) in api.proxy.js, which results in this error: "InvalidShopError: Received invalid shop argument"
 
Any guidance on how to access the admin api so I can run the graphql query would be greatly appreciated!
 

This is my route file, api.proxy.js, in app/routes

import { json } from "@remix-run/node";
import { cors } from "remix-utils/cors";
import { authenticate, unauthenticated } from "../shopify.server";

const GET_SHOP_INFO = `simple graphql query for testing`;

export const loader = async ({ request }) => {

// Used when routing request through an app proxy. Causes cors error when app proxy fetched from Checkout.jsx.
//const { admin } = await authenticate.public.appProxy(request); 

// Used when fetch bypasses app proxy. Causes this error: "InvalidShopError: Received invalid shop argument"
//const { admin } = await unauthenticated.admin(request);

// Used when fetch bypasses app proxy. Causes this error: "ERR_INVALID_URL"
const { admin } = await authenticate.admin(request);
 
  if (request.method === 'OPTIONS') {
    const response = json({
      status: 200,
    });
    return await cors(request, response);
  }
 
// Test output to see if the admin api client is successfully created above
  const response = json({
    ok: true,
    message: 'Success',
    data: 'proxy loader response',
  });
 
  //const response = await admin.graphql(GET_SHOP_INFO);

  return cors(request, response);
}

 

attaboiaj
Shopify Partner
29 4 13

@Yam No idea about proxy never userd

but you can try this
https://shopify.dev/docs/apps/build/online-store/display-dynamic-data

mikefortney
Shopify Partner
16 0 10

I believe your issue here is that you are trying to authenticate a checkout extension request using the admin authorization method. Try this instead if the call is coming from your checkout extension:
`await authenticate.public.checkout(request);`

That will authenticate a request coming from a checkout extension.

You may also need to use `await authenticate.public.customerAccount(request)` if the request is being made from a customer account page or the order status page.

Yam
Shopify Partner
3 0 0

Thank you for the suggestion.

 

 

const { admin } = await authenticate.public.checkout(request);

 

and 

 

const { admin } = await authenticate.public.customerAccount(request);

 

both authenticate successfully, but I do not have access to the admin api, which I'm pretty sure I need to run graphql queries.

 

Just for clarification, I'm calling this route code from the Thank You page (might need to do so from Order Status as well).

Yam
Shopify Partner
3 0 0

I eventually figured out the main cause of the issue I was having. Perhaps I missed it in the documentation, but you first have to click on the app in the store admin so it can create a session token. Once I did that, the admin object was defined and I could continue on. This post is what helped me figure it out and provides more details.

 

The browser console messages can be a little misleading at times - be sure to look at the server logs to see what's really going on.

 

I'm hosting my app on DigitalOcean and have to click on the app in my store admin every time I redeploy the app because the session token is destroyed during the rebuild. This could be a pain going forward; perhaps there's a way to make the token persist after app builds, but I haven't looked into that yet.