Shopify App 'Refused to Connect'? Navigating React Router v8 & App Bridge Issues

Hey folks! Ever been deep into developing your Shopify app, feeling good about your progress, and then BAM! You hit that dreaded "apps.shopify.com refused to connect" message? It's a frustrating wall to hit, especially when you're up against a Shopify compliance deadline. We recently saw a great discussion in the community about this exact issue, and I wanted to break down the insights shared by some of our sharpest developers.

Andrew, a store owner and developer, kicked off the thread with a classic scenario: his Shopify app, hosted on Fly.io, worked perfectly on the first load. But as soon as he navigated from the homepage to a customizer page and then back, he was greeted with a blank screen and that "refused to connect" error. This meant his app was breaking out of its iframe, causing an error loop. He was seeing /_root.data 202 in his logs, indicating a redirect was happening, but it was leading to the wrong place.

Shopify app refused to connect icon

The Core Problem: React Router & Shopify App Bridge Mismatch

Lumine, a seasoned expert in the community, quickly zeroed in on the likely culprit: a version mismatch with react-router. Andrew was using react-router: 8.3.0, but the @shopify/shopify-app-react-router package (v2.0.0) declares a peer dependency on react-router: ^7.6.2. This means the Shopify adapter wasn't designed to work with React Router v8, which introduced significant changes to its single fetch pipeline.

In essence, the Shopify adapter relies on specific hooks in React Router v7 to catch reauthentication responses and handle them gracefully within the iframe using App Bridge. With v8, these hooks don't fire as expected. The .data request (which is what /_root.data 202 signifies) comes back unhandled, causing the browser to perform a top-level navigation. And when that navigation points to apps.shopify.com, it gets blocked by X-Frame-Options, leading to the dreaded "refused to connect" message.

Diving Deeper: The Redirect Target & Missing Authorization

MayraApps and Lumine further clarified that the _root.data 202 isn't an error in itself; it simply means your root loader threw a redirect. The critical detail was where that redirect was pointing. Andrew's response body showed it was redirecting to his public App Store listing (e.g., https://apps.shopify.com/zentra-announcement-bar), not an admin URL.

This is a key distinction. Shopify's libraries typically redirect to https://{shop}/admin/oauth/install for installation, not your public listing. If your app is redirecting to the public listing, it suggests a deeper issue: either the library can't resolve the shop context at all, or there's a hardcoded redirect in your own app's code.

Another crucial piece of the puzzle is the Authorization header. Lumine pointed out that if the _root.data request went out without this header, authenticate.admin would treat it as a document request. This often happens when App Bridge isn't properly initialized within the iframe. App Bridge is responsible for adding that header, so if window.shopify is undefined in your console, that's a huge red flag.

A common reason for App Bridge failing to load is an empty or missing data-api-key in the script tag. This usually stems from process.env.SHOPIFY_API_KEY not being set as a runtime secret in your hosting environment (like Fly.io), but rather only as a build argument. Without that API key, App Bridge can't initialize, and your app loses its crucial connection to the Shopify admin context.

Actionable Steps to Resolve the "Refused to Connect" Error

Based on the community's collective wisdom, here's a step-by-step guide to tackling this issue:

1. Pin Your React Router Version

The first and most critical step is to ensure your react-router version is compatible with the Shopify adapter. Andrew's problem was using v8 when v7 was expected.

  1. Check current versions: Run npm ls react-router to see what versions are installed and if there are any duplicates or hoisted versions.
  2. Force v7.18.2: Add an "overrides" block to your package.json to force react-router and react-router-dom to 7.18.2. (If you're using Yarn, this is "resolutions"; for pnpm, it's pnpm.overrides).
    "overrides": {
      "react-router": "7.18.2",
      "react-router-dom": "7.18.2"
    }
  3. Clean install: Delete your node_modules folder and your lockfile (package-lock.json or yarn.lock), then run npm install (or yarn install, pnpm install).
  4. Verify again: Run npm ls react-router to confirm only 7.18.2 is present. Also, check your vite plugin version; @react-router/dev 7.18.2 is compatible with vite ^5.1.0 || ^6.0.0 || ^7.0.0 || ^8.0.0, so versions like 7.3.6 are fine.
  5. Rebuild and deploy: npm run build && fly deploy (or your equivalent deploy command).

2. Verify App Bridge Initialization

If the version pinning doesn't solve it, or if you suspect App Bridge isn't working, these checks are crucial:

  1. Check window.shopify: Open your browser's developer console while your app is loaded in the Shopify admin iframe. Type window.shopify. If it returns undefined, App Bridge isn't loading.
  2. Check SHOPIFY_API_KEY: View your app's source code (within the iframe) and inspect the App Bridge script tag. It should look something like . Ensure data-api-key has a value. If it's empty, your SHOPIFY_API_KEY environment variable isn't being picked up at runtime. For Fly.io, Lumine suggested running fly ssh console -C "printenv SHOPIFY_API_KEY" to confirm it's set as a runtime secret.

3. Identify and Correct Problematic Redirects

If your app is still redirecting to apps.shopify.com, you need to find where that URL is coming from and adjust your redirect logic.

  1. Inspect _root.data response: In your DevTools Network tab, click on the _root.data request and check its Response tab. The problematic URL will be sitting there in plain text.
  2. grep for the URL: Use the command Lumine provided to find where apps.shopify.com is referenced in your codebase:
    grep -rn "apps.shopify.com" app/ server/ *.ts *.tsx *.js *.jsx
    If it's not in app/ or server/, widen your search to node_modules, as a dependency might be carrying it.
  3. Adjust redirect logic: If you find a custom redirect that sends users to your app listing page, you need to modify it. The Shopify adapter's authenticate.admin helper has an "escape hatch" for this. Instead of a plain redirect, you should pass { target: '_top' } to ensure App Bridge handles the navigation correctly and breaks out of the iframe without error. This will typically result in a 401 with an X-Shopify-API-Request-Failure-Reauthorize-Url header, which App Bridge understands and uses to navigate the top frame.
    const { redirect } = await authenticate.admin(request);
    throw redirect("https://apps.shopify.com/zentra-announcement-bar", { target: "_top" });
    Important caveat: This target: '_top' approach relies on App Bridge patching fetch and adding an Authorization header. If App Bridge isn't alive (as per step 2), this 401 path won't fire, and you'll still get a 302. So, ensure App Bridge is working first!

This "refused to connect" issue is a classic example of how deeply integrated Shopify apps are with the platform's infrastructure. Keeping your dependencies aligned, ensuring App Bridge is initialized, and correctly handling redirects are paramount for a smooth user experience and staying compliant. Thanks to the detailed analysis from Lumine and MayraApps, Andrew (and hopefully you!) now have a clear path to getting that app back on track and passing those compliance deadlines.

Share:

Use cases

Explore use cases

Agencies, store owners, enterprise — find the migration path that fits.

Explore use cases