Fix: browser navigations to /scope and /api/intake returned 404 - #73
Merged
Conversation
Cloudflare's asset router applies `not_found_handling` only to requests carrying `Sec-Fetch-Mode: navigate`. A normal request that misses the asset manifest is passed to the Worker; a navigation that misses is answered with 404.html and the Worker is never invoked. `/scope` and `/api/intake` are the only routes here rendered on demand, so neither is in the manifest. Every browser navigation to the scoping form got the 404 page, and because a form POST is also a navigation, the intake endpoint was unreachable from a browser at all. The revenue path was dead from the moment the property became reachable. Introduced with the 404 page itself. sites/www carries the same `not_found_handling` and is unaffected only because it ships no 404.html for the router to serve, so its misses still fall through to the Worker and its /contact form works. A comment there records the trap, since adding a 404 page to that site would break it the same way. Nothing caught it. The property had no DNS record until today, so nothing could navigate to it, and every check run against it used curl, which sends no Sec-Fetch-Mode and therefore always took the Worker path and always returned 200. `run_worker_first` lists the two on-demand routes rather than being set to `true`, so the Worker runs only for those and the static files are still served by the asset router without invoking it. Verified locally against `wrangler dev` with `Sec-Fetch-Mode: navigate`: /scope and /scope/ return 200, a POST to /api/intake returns 303 to `?error=captcha` rather than 404, static routes still 200, and an unknown path still returns the real 404 page. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J53xszP5CdZLXfhRJWsSyv
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The intake form was unreachable from a browser from the moment the property went live. Not the Turnstile hostname, which I blamed earlier today — that was wrong.
Mechanism
Cloudflare's asset router applies
not_found_handlingonly to requests carryingSec-Fetch-Mode: navigate. A normal request that misses the asset manifest is passed to the Worker. A navigation that misses is answered with404.html, and the Worker is never invoked./scopeand/api/intakeare the only routes on this site rendered on demand, so neither exists in the manifest. Every browser navigation to the scoping form got the 404 page — and since a form POST is also a navigation, the intake endpoint could not be reached from a browser at all.Reproduced exactly, on the live site:
Why it survived every check
404.htmlfor the router to serve, so misses fell through to the Worker.sites/wwwhas the identicalnot_found_handlingand is fine only because it ships no404.html. Its/contactworks. A comment now records that trap, because adding a 404 page there would break it the same way.curl, which sends noSec-Fetch-Mode— so it always took the Worker path and always returned 200. The bug was invisible to the exact tool used to verify it.The fix
run_worker_first: ["/scope", "/scope/", "/api/*"]— a route list rather thantrue, so the Worker runs only for the two on-demand paths and the static files keep being served by the asset router without invoking it.not_found_handling: "404-page"stays, so genuine misses still get the real 404 page.Verified locally
wrangler dev, every request carryingSec-Fetch-Mode: navigate:GET /scopeGET /scope/POST /api/intake/scope?error=captchaGET /method/,/privacy/,/experts/robert-jacques/GET /nopeThe POST returning
?error=captchais correct — no Turnstile token in a curl request.🤖 Generated with Claude Code
https://claude.ai/code/session_01J53xszP5CdZLXfhRJWsSyv