Apps.shopify.com refused to connect

When navigating my shopify app, home page works but as soon as I navigate to the customizer page. Then right away navigate back to the homepage, I always get a blank page with an icon and message saying: “apps.shopify.com refused to connect.”

image

As a temporary solution to make the app work, i’ve duplicated the home page by putting it in app nav links which doesn’t cause the issue.

I believe the issue is something where a redirect is causing the app to break out of the iframe and causing an error loop.

Versions:

@shopify/shopify-app-react-router v2.0.0
react-router: 8.3.0
@shopify/app-bridge-react: 4.2.4

Every navigation triggers GET /_.data returning 202

This causes a full top-level navigation to apps.shopify.com breaking the iframe.

App works on first load but breaks after navigating between routes.

Fly.io hosted.

App has a compliance deadline from Shopify - spend days trying different code configurations but nothing’s working.

Is there a specific configuration im missing, potentially a versions mismatch or simply my javascript or a bit of both?

Check what react-router actually resolved to in your lockfile. @shopify/shopify-app-react-router 2.0.0 declares react-router: ^7.6.2 as its peer dependency, and that range stops at 7.x. You are on 8.3.0.

React Router 8.0.0 landed 17 June 2026 and 8.3.0 on 22 July, so a fresh install of react-router now pulls v8 by default. The Shopify adapter has not followed. Version 2.0.0 shipped 10 August 2026 and still pins the peer to ^7.6.2, so there is no version of the adapter today that claims to work with what you have installed.

My read on the 202, and this part is inference rather than something I have traced in their source, is that the adapter hooks the v7 single fetch pipeline to catch a reauth response and hand it to App Bridge as an in frame redirect. Under v8 that hook does not fire, the .data request comes back unhandled, and the browser falls back to a top level navigation. Top level means it leaves the iframe, and the URL it leaves to is on apps.shopify.com, which sends X-Frame-Options and gives you the refused to connect box. Fits your symptom of first load fine, every navigation after that broken.

Run npm ls react-router before you change anything, since something else in the tree may be hoisting v8. Then pin to 7.18.2, drop node_modules and the lockfile, reinstall.

Your duplicated home page workaround is consistent with this. Putting it in the nav links makes it a full page load through the Shopify admin rather than a client side navigation, so no .data request happens and nothing has to handle the response.

If it still breaks after pinning, check whether react-router-dom or a vite plugin is resolving to 8 separately. What compliance deadline are you up against?

@andrewjforster5 Confirmed the versions above if it helps before you start pulling things apart. 2.0.0 is the current release, published 10 August, peer range is react-router ^7.6.2, and 7.18.2 is the highest 7.x.

On the hoisting question, dont hunt for whats pulling 8, just force it. In package.json:

json

"overrides": {
  "react-router": "7.18.2",
  "react-router-dom": "7.18.2"
}

Thats npm. Yarn calls it resolutions, pnpm nests it under pnpm.overrides.

Then delete node_modules and the lockfile, reinstall, and npm ls react-router should come back with a single entry. If two show up you still have a duplicate in the tree, and thats worth finding before you spend another day on config.

Hi Lumine,

Thanks for your reply.

The deadline consists of a few things, 2 of them i have sorted. The last one being specific to this topic.

How my app worked before the deadline started was when navigating back to the home page, from the customizer, it would show a login message that i had coded in. The login didn’t work, and when entering the shopify store url it would then show the same “apps.shopify.com refused to connect” thus breaking the iframe.

So after recieving the email from shopify about the deadline, I removed the login screen, and now its replaced with the instant refused to connect - which proved tricky to solve.

Technically i’ve fixed 2.3.1 Initiate installation from a Shopify-owned surface, by removing the login screen. So it’s not requesting a store url, however still wondering if apps.shopify.com counts as this requirement also, not sure.

But the blank screen apps.shopify.com is a huge issue regardless if it’s part of the deadline requirements or not. So i’m treating it as if it is just incase.

As for the dealine dates and stages, shopify said:

  • Starting August 26, 2026, your app will be demoted and start ranking lower in Shopify search results and recommendations.
  • On September 4, 2026, your app will be delisted and merchants will no longer see it on the Shopify App Store.
  • On September 29, 2026, your case will be escalated to Partner Governance, which may result in your Partner account being terminated.

I’ve implemented your approach to this through pinning to 7.18.2 on node_modules and lockfile, re-installing.

ran npm ls react-router before and after, it showed duplicates and packages that were still on v8. Now after pinning etc reinstalling the tree looks clean node_modules are now at 7.18.2

npm ls react-router:

├─┬ @shopify[email removed]
│ └── [email removed] deduped
├─┬ @shopify[email removed]
│ └── [email removed] deduped
├─┬ [email removed]
│ └── [email removed]
├─┬ [email removed]
│ └── [email removed] deduped
├─┬ [email removed]
│ └── [email removed] deduped
└── [email removed]

vite plugin was resolving to 8 also so re-installed etc to 7.3.6. (Is that the right vite version i need?)

npm ls vite:

├─┬ @react-router[email removed]
│ ├─┬ [email removed]
│ │ └── [email removed] deduped
│ └── [email removed] deduped
├─┬ [email removed]
│ └── [email removed] deduped
└── [email removed]

committed and pushed then ran: npm run build && fly deploy.

Still get refused to connect and on my fly logs /_root.data 202 when error occurs

In case i’m missing any of the modules or the versions, would you like me to share package.json. If so, the dependencies block or other areas?

Or any of my roots files?

Thanks again,

Andrew

Andrew, the pin was worth doing, but I had the 202 wrong and I should correct that before you chase it further.

202 is not an error here. In react-router 7.18.2 the constant is literally SINGLE_FETCH_REDIRECT_STATUS = 202. When a loader throws a redirect on a .data request, the server deletes the Location header, sets content-type to text/x-script, puts the target URL inside the turbo-stream body, and returns 202. So /_root.data 202 only tells you your root loader threw a redirect. It would have done the same on v8. Pinning was never going to move that number.

What breaks the frame is where the redirect points. Client side, react-router looks at the location, and if it is an absolute URL on a different origin it calls window.location.assign instead of routing. That is a real navigation of your iframe, and the shopify.com surface it lands on refuses framing. That is your box.

So read the body of the 202, not the status. DevTools, Network tab, click _root.data, Response. The URL is sitting in there as plain text.

Now the part that points at a cause. In @shopify/shopify-app-react-router 2.0.0, a data request that carries the session token never gets a plain redirect. helpers/respond-to-invalid-session-token.js computes isDocumentRequest = !request.headers.get('authorization'), and when the header is present the failure comes back as 401 with X-Shopify-API-Request-Failure-Reauthorize-Url, which App Bridge catches and handles inside the frame. You are getting 202, not 401. That means the _root.data request went out with no Authorization header, so authenticate.admin treated it as a document request. admin/authenticate.js gates that whole branch on !getSessionTokenHeader(request), and inside it ensureAppIsEmbeddedIfRequired throws redirect(await api.auth.getEmbeddedAppUrl(...)) whenever embedded=1 is missing from the query. Absolute, cross origin, and you are back at the box.

That fits your symptom exactly. First load works because Shopify puts id_token, shop and host in the URL, so the server authenticates off the URL param and never needs the header. Every client navigation after that has neither the param nor the header.

App Bridge is what adds the header, so check it is actually alive in the frame. In the console type window.shopify. Undefined means it never loaded. Then view source and look at the tag. AppProvider renders it as <script src="https://cdn.shopify.com/shopifycloud/app-bridge.js" data-api-key={apiKey}> and apiKey comes from your loader returning process.env.SHOPIFY_API_KEY. On Fly that has to exist as a runtime secret. If you only set it as a build arg, the tag renders with an empty data-api-key and App Bridge never initialises. fly ssh console -C "printenv SHOPIFY_API_KEY" settles that in one command.

On vite, you did not need the downgrade. @react-router/dev 7.18.2 declares vite ^5.1.0 || ^6.0.0 || ^7.0.0 || ^8.0.0, so 7.3.6 is fine and so was 8.

If you post anything back, the two useful things are the request headers on _root.data (is Authorization there) and the response body (which URL). Those answer it without anyone needing to read your package.json.

Lumine, when i check the network tab when the refused to connect is there, here is the response tab:

But if i click on _root.data just before the refused to connect page loads. The response tab gives:

[[“SingleFetchRedirect”,1],{“_2”:3,“_4”:5,“_6”:7,“_8”:7,“_9”:7},“redirect”,“https://apps.shopify.com/zentra-announcement-bar",“status”,302,“revalidate”,false,“reload”,"replace”]

That response body is the whole answer, Andrew. The redirect target is your own App Store listing.

Which rules out the library. I pulled @shopify/shopify-app-react-router 2.0.0 and @shopify/shopify-api 14.0.0 and grepped both for apps.shopify.com. Zero hits in either package. The one place the library ever sends you to install is helpers/redirect-to-install-page.js, and it builds https://{shop}/admin/oauth/install with client_id and scope on it, never the listing URL. Nothing in the Shopify stack produces that string, so it came from your code or from one of your own dependencies.

That also kills the guess I gave you last time. I said the missing 401 meant the _root.data request went out without an Authorization header. That only followed if the library threw the redirect, and it did not, so drop that thread.

One command should find the line.

grep -rn "apps.shopify.com" app/ server/ *.ts *.tsx

Now the part that still bites after you find it. A 302 to another origin cannot work from inside the admin iframe no matter which line throws it. Look at what your payload decodes to. status 302, revalidate false, reload false, replace false. That is a plain redirect(url) with default options.

The library has an escape hatch for this exact case. The redirect you get back from authenticate.admin is not the react-router one. In authenticate/admin/helpers/redirect.js it branches on target first, and the default target is _self, which just returns reactRouter.redirect and hands you the 302 you already have. Pass { target: ‘_top’ } and it falls through to the isDataRequest branch, which throws redirectWithAppBridgeHeaders instead. That is a 401 carrying X-Shopify-API-Request-Failure-Reauthorize-Url, and App Bridge reads that header and navigates the top frame. No iframe involved, so nothing to refuse framing.

const { redirect } = await authenticate.admin(request);
throw redirect("https://apps.shopify.com/zentra-announcement-bar", { target: "_top" });

One caveat on that branch. isDataRequest checks for an Authorization header on the request, so if App Bridge is not patching fetch the 401 path never fires and you land back on a 302. Worth confirming while you are in there.

What is that redirect supposed to do in the first place? If it fires whenever a session lookup comes back empty, it will loop for merchants who already have the app installed, and the frame error is just the first symptom you happened to see.

Hi MayraApps,

Thank you for confirming the versions. I tried the overrides but it’s not solving it, the versions tree looks correct now. I think its something with the url inside the turbo-stream body, returning the /_root.data 202.

Maybe it could be that my url is hardcoded?

Or a general mismatch in the code of my app.jsx and root.jsx.

Would you like me to share part of the code from these files?

Also checkout my latest reply to Lumine, where I’ve provided the header response body in the 202 that appears.

Cheers,

Andrew

@andrewjforster5 One detail in your response body worth pulling out, because it doesnt quite match the branch lumine described.

getEmbeddedAppUrl returns an admin URL, something on admin.shopify.com pointing at the app inside a store. What youve actually got in that turbo-stream body is your public App Store listing page. Thats a different destination, and its the one the library uses when it cannot resolve which shop the request belongs to at all, rather than when it merely knows the shop but sees embedded=1 missing. Im inferring that from the URL shape rather than having traced it, so treat it as a lead.

If thats right its a slightly worse symptom than embedded=1, and it makes lumines SHOPIFY_API_KEY check the first thing I would do, since a dead App Bridge means no Authorization header and no shop context on any client navigation.

While youre in there, check what your links actually navigate to. If a Link points at a bare path with no query string, and App Bridge isnt alive to add the header, the .data request goes out with nothing identifying the shop on it. Which is exactly the state that produces a redirect to your listing page.

So the two commands are the printenv one lumine gave you, and typing window.shopify in the console inside the frame. If that comes back undefined, everything else here is downstream of it.

Andrew, before you paste any code, run the one command. It settles more than either of us guessing at it.

grep -rn “apps.shopify.com” app/ server/ *.ts *.tsx *.js *.jsx

On the reading Mayra offered, I want to be exact about what is established and what is not. I pulled both tarballs again just now, @shopify/shopify-app-react-router 2.0.0 and @shopify/shopify-api 14.0.0, and grepped the full package contents of each. Zero hits for apps.shopify.com in either one. So there is no branch in those two libraries that sends you to your public listing. Not for an unresolvable shop, not for a missing embedded param, not for anything. The only install redirect they construct is https://{shop}/admin/oauth/install with client_id and scope on it. That is a grep result rather than a trace, so it holds regardless of which code path you are on.

Which leaves the string coming from your side, and the grep tells you where in one shot. If it hits in app/ or server/, that is your answer and you can stop there. If it comes back empty, widen it once to node_modules, because a dependency of yours can carry the string just as easily as your own route file can.

Worth thirty seconds before we start reading app.jsx, because the two cases have completely different fixes.

I’m a bit late to get back to you on this, but what you’ve said here sums up exactly what the problem was. I had a “throw redirect” in there, the redirect url being the app store listing.

Since i removed it the app navigation works completely. No more refused to connect.

My reasoning for having the url is when visiting the url the app is hosted from, it immediately brings them to that app store listing so there is no confusion on the url. However this broke the app itself.

Cheers for spotting it! :tada:

Thanks for getting back.

I found out that in my route.jsx file i had: throw redirect(“Zentra Announcement Bar - Promote sales with an easy-to-customize announcement bar | Shopify App Store”);

Turns out I had manually added this app store listing url. Ever since i’ve removed this line the navigation work perfectly. :flexed_biceps:

Thats awesome, congrats :slight_smile: