Skip to content

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: 1042

fetchGame 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/web to tabletop-tracker-api.
  • Read the catalogue through the binding when it is bound, and over PUBLIC_API_URL when 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:

Terminal window
pnpm --filter @tabletop/web build
python3 -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:

Terminal window
cd apps/web && pnpm exec wrangler dev -c dist/server/wrangler.json --remote
curl -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8787/games/catan

Locally, with both dev processes up:

Terminal window
pnpm --filter @tabletop/api dev # terminal one
cd 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:

beforeafter
/games/catanstatus=404 body=error code: 1042status=500 body={"error":"internal error"}
/games/CATANstatus=404 body=error code: 1042status=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.