WI-064: The web Worker cannot reach the API
WI-064: The web Worker cannot reach the API
Problem
Every game page on the deployed site returned 404 — including Browse a game,
the only link out of the home page (Base.astro:61).
apps/web read the catalogue by fetching the API’s workers.dev URL. That
works from a laptop, from CI and from wrangler dev --local. It does not work
from a deployed Worker: Cloudflare refuses a Worker-to-Worker subrequest
within the same workers.dev zone, and answers 404 with a body of:
error code: 1042fetchGame treats 404 as “no such game” — correct for a real 404 — returns
null, and the page renders its 404. No exception, no log, and nothing in
either Worker’s tail, because the request never arrived. Every Worker on the
account shares one *.workers.dev subdomain, so this was never fixable by
naming.
ADR-0024 records the decision and the two alternatives.
In scope
- A service binding from
apps/webtotabletop-tracker-api. - Read the catalogue through the binding when it is bound, and over
PUBLIC_API_URLwhen it is not.
Out of scope
- The API’s own 500 on
/games/:slug— an unapplied migration, and a separate work item (WI-063, #117, in review alongside this one). Until it is cleared, a game page returns 500 rather than 404: a fault reported instead of a fault disguised as a missing game. That is the improvement this item can make on its own. - Moving the API to a custom domain. Considered and rejected in ADR-0024.
apps/mobile, which is a browser client and never had this problem.
Definition of done
- The generated Worker config carries a service binding to
tabletop-tracker-api. - The catalogue is read over the binding on Workers, and over HTTP everywhere else.
- A game page on the real edge reaches the API — proven by the API’s own responses, not by an absence of errors.
- Tests cover both transports, including that a 404 stays “missing game” and a 500 does not.
- Local development still works, with the route documented.
Verification
The binding is declared in apps/web/wrangler.jsonc, which @astrojs/cloudflare
merges into the config it generates:
pnpm --filter @tabletop/web buildpython3 -c "import json;print(json.load(open('apps/web/dist/server/wrangler.json'))['services'])"# [{'binding': 'API', 'service': 'tabletop-tracker-api'}]On the real edge, without deploying:
cd apps/web && pnpm exec wrangler dev -c dist/server/wrangler.json --remotecurl -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8787/games/catanLocally, with both dev processes up:
pnpm --filter @tabletop/api dev # terminal onecd apps/web && pnpm exec wrangler dev -c dist/server/wrangler.json --local# env.API (tabletop-tracker-api) Worker local [connected]curl -s http://127.0.0.1:8791/games/catan | grep '<title>'# <title>Catan (1995) — Tabletop Tracker</title>astro dev needs no second process: with no binding present it reads over
PUBLIC_API_URL, exactly as before.
Notes
The fault was invisible to every tool that did not run on Cloudflare. It
reproduces only under wrangler dev --remote; --local renders the page
correctly. What identified it was logging the response the Worker actually
received, on the edge:
| before | after | |
|---|---|---|
/games/catan | status=404 body=error code: 1042 | status=500 body={"error":"internal error"} |
/games/CATAN | status=404 body=error code: 1042 | status=400 body={"error":"invalid slug"} |
The second row is the one that proves it: invalid slug is the API’s own
validation (ADR-0022),
and nothing else on the internet would answer that.