A space to discuss online store customization, theme development, and Liquid templating.
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.
Solved! Go to the solution
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.
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
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 `
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.
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.
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
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?
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"
}
})
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?
Thank you so much Danh11 for your response
this is the error I get from my browser when I trigger the fetch request
I also believe the route is being hit as I get this error in my terminal
this is how my js in the liquid file looks like
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;
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.
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.
Were you able to solve it, i am stuck at same point
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
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
Did you try it by running by hosting urls somewhere
I hosted my app on a VPS and I am getting the cors error
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.
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-...
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:
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,
);
}
}
}
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
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?
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
It is working for me as well, I made a rookie mistake where I was sending requests to the wrong url
Haha this response gonna saves a day for everyone.
it's super weird that how things works here.
Glad my solution helped
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}`,
},
});
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);
}
@Yam No idea about proxy never userd
but you can try this
https://shopify.dev/docs/apps/build/online-store/display-dynamic-data
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.
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).
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.
You saved me a lot of time! Thank you very much!
Glad it helped man, I know the pain