Shopify / Shopify/hydrogen

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

Open
#3,979 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
2.1k
Forks
442
Avg merge
4d 19h
Merged PRs (30d)
24

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with handleShopifyRoutes, createProxyInterceptor, handleWellKnownProxy, and handleShopifyApiProxy to understand the existing proxy contract and route matching. Reproduce the curl requests against an /a/downloads path, then verify that the interceptor preserves Shopify's status, body, content headers, and redirect location without requiring callers to add _fd=0 or browser headers.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
backend, web-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
67/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.