handleShopifyRoutes has no interceptor for app-proxy paths (/a/*) - Hydrogen preview 1
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.
- Install Digital Downloads, attach a file to a product, place a test order.
- Copy the download link from the order email:
https://<primary-domain>/a/downloads/-/<grant>/<token> - 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">
- Run a storefront that calls
handleShopifyRoutesin 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
- No interceptor matches
/a/*, sohandleShopifyRoutesreturnsnull, the path falls through to framework routing, misses, and thehandleShopifyRedirectsfallback 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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