Skip to content

handleShopifyRoutes has no interceptor for app-proxy paths (/a/*) - Hydrogen preview 1 #3979

Description

@Eigilak

What is the location of your example repository?

No repro repo — reproducing needs a store with the Digital Downloads app, a digital product, a custom primary domain, and a live grant token from an order email. The curl commands under Steps to Reproduce show the behaviour against any such store. Happy to put together a minimal scaffold if that's needed to action this.

Which package or tool is having this issue?

Hydrogen

What version of that package or tool are you using?

@shopify/hydrogen@2026.10.0-preview.1

What version of React Router 7 are you using?

N/A — TanStack Start (@tanstack/react-start), not React Router

Steps to Reproduce

HEYOOO Shopify Peeps!

Using Shopify's own Digital Downloads app, since it produces a URL customers already have in their inbox. Any /a/* path behaves the same.

Env: @shopify/hydrogen@2026.10.0-preview.1, TanStack Start on Cloudflare Workers (workerd via @cloudflare/vite-plugin), storeDomain = <shop>.myshopify.com, custom primary domain.

  1. Install Digital Downloads, attach a file to a product, place a test order.
  2. Copy the download link from the order email: https://<primary-domain>/a/downloads/-/<grant>/<token>
  3. Confirm Shopify serves it on the primary domain:
curl -s -o /dev/null -w '%{http_code}\n' \
  -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36' \
  -H 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
  -H 'sec-fetch-dest: document' -H 'sec-fetch-mode: navigate' \
  "https://<primary-domain>/a/downloads/-/<grant>/<token>"
# 200, HTML containing <div id="digital-downloads-proxy">
  1. Run a storefront that calls handleShopifyRoutes in request middleware, then request the same path:
curl -s -o /dev/null -w '%{http_code}\n' "http://localhost:3000/a/downloads/-/<grant>/<token>"
# 404

Proxying it yourself hits two walls. This is what an interceptor would do, since createProxyInterceptor resolves upstream against storefrontClient.storeUrl:

# storeUrl is *.myshopify.com, and the app proxy 301s back to the primary domain
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' \
  "https://<shop>.myshopify.com/a/downloads/-/<grant>/<token>"
# 301 https://<primary-domain>/a/downloads/-/...  — a loop, the storefront is that domain

# ?_fd=0 stops the redirect, but a non-browser request still 404s
curl -s -o /dev/null -w '%{http_code}\n' \
  "https://<shop>.myshopify.com/a/downloads/-/<grant>/<token>?_fd=0"
# 404, empty body

# ?_fd=0 plus the browser headers from step 3
# 200

The /download subpath is the same: 404 bare, 302 to a 24h-signed storage.googleapis.com URL with the headers present.

Expected Behavior

/a/* proxied to the shop origin by handleShopifyRoutes, with the response passed through as-is — status, body, content-type, content-disposition, location on redirects. Same contract handleWellKnownProxy and handleShopifyApiProxy already provide.

So /a/downloads/-/<grant>/<token> returns Shopify's 200, .../download?download=<file> returns Shopify's 302 to the signed URL, and links already in customers' inboxes survive a cutover.

The *.myshopify.com → primary-domain 301 should be handled inside the interceptor. Callers shouldn't need to know _fd=0 exists.

An interceptor built on the existing createProxyInterceptor, matching /^\/(?:a|apps|tools|community)(?:\/|$)/, would cover it. Opt-in is fine if proxying arbitrary /a/* isn't a safe default — but then it needs documenting, because today there's no support and no warning.

Actual Behavior

  1. No interceptor matches /a/*, so handleShopifyRoutes returns null, the path falls through to framework routing, misses, and the handleShopifyRedirects fallback finds no redirect for it. Nothing logs. The first signal is a customer with a dead download link.

The workaround is re-implementing the proxy by hand against two undocumented behaviours (_fd=0, browser-header sniffing). Digital Downloads has no API — no DigitalDownload type in the Admin schema — so that means scraping Shopify's own markup for the title, file name, size and download token.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions