fix(web-e2e): probe a fresh url each poll, and stop blaming next dev for answering#785
Merged
Merged
Conversation
…for answering CI went red on main at 7d07827 in `web-e2e`, on a commit whose `apps/web` and `packages/` trees are byte-identical to the green run before it (#783 touched only the IndexNow scripts and .gitleaksignore). It is the boot probe, not the product. What the failing run shows: ✓ Ready in 352ms GET /dev-session 307 in 1009ms ○ Compiling /[...legacy] ... GET /org/…/payload 404 in 4.5s (next.js: 4.1s) GET /org/…/payload 404 in 108ms (next.js: 10ms) … 542 of them, then the 180s timeout The server answered every single probe, promptly. `next dev` began serving before its scan of `src/app` reached the deepest route, so the catch-all `(app)/[...legacy]` claimed the url, compiled, and 404'd — and every later poll for that SAME url came back in ~2ms of Next time with no second "Compiling" line, i.e. from a cached match rather than a fresh resolution. The green run on the identical tree asked once, after the table was complete, and got its 307 immediately. Which side of that race a run lands on is the only difference. So re-asking one CONSTANT url cannot recover: the poll loop re-sends exactly the url whose resolution already went to the catch-all. `probeUrlFor` now mints a fresh uuid per dynamic segment on every call, making each poll a url `next dev` has never resolved — matched against the route table as it stands at that moment, which is the question the probe means to ask. The uuid is generated inside the function rather than passed in so a caller cannot hoist it back out of the loop and silently restore the constant. Also: the timeout said "next dev did not answer" for both failure modes, and in the one that actually happens that is a lie — it sent this investigation through process spawning, port binding and the database before the logs showed a healthy server the whole time. It now reports which failure it was and what the last probe saw. Verified: 18/18 deepest-route unit tests (the distinctness test is RED before this change — 20 calls, 1 unique url), 25/25 web e2e, root typecheck 33/33, lint, prettier. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019LhaMQFjLyhrrMxs1sxTZV
choraria
marked this pull request as ready for review
July 24, 2026 08:08
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.
Why
CI is red on
mainat7d07827in theweb-e2ejob.#783touched only the IndexNow scripts and.gitleaksignore—apps/webandpackages/are byte-identical to the green run before it (git diff 74e921ed 7d078279 -- apps/web packages/is empty). This is the boot probe, not the product.What the failing run actually shows
The server answered every probe, promptly, for the entire three minutes.
next devbegan serving before its scan ofsrc/appreached the deepest route, so the catch-all(app)/[...legacy]claimed the url, compiled, and 404'd. Every later poll for that same url came back in ~2ms of Next time, with no secondCompilingline — a cached match, not a fresh resolution. The green run on the identical tree asked once, after the table was complete, and got its 307 on the first request. Which side of that race a run lands on is the only difference between them.The fix
Re-asking one constant url cannot recover: the loop re-sends exactly the url whose resolution already went to the catch-all.
probeUrlFornow mints a fresh uuid per dynamic segment on every call, so each poll is a urlnext devhas never resolved — matched against the route table as it stands at that moment, which is the question the probe means to ask.The uuid is generated inside the function rather than accepted as a parameter, so a caller cannot hoist it out of the poll loop and quietly restore the constant. The route (the
src/appwalk) is still hoisted; only the url is per-poll.Second change: the timeout said
next dev did not answerfor both failure modes, and in the one that actually happens that is a lie. It cost this investigation a trip through process spawning, port binding and the database before the logs revealed a healthy server the whole time. It now distinguishes the two and reports what the last probe saw.Verification
deepest-routeunit tests 18/18. The new distinctness test is RED before this change — 20 calls produced 1 unique url (expected 1 to be 20).apps/webe2e 25/25, run three times.pnpm typecheck33/33 (@webhook-co/weba fresh cache miss),pnpm lint639 pass, prettier clean.Honest caveat for the reviewer
The stickiness mechanism is inferred from the CI logs (single
Compilingline, 4.1s → 2ms of Next time, no recovery in 180s), not reproduced locally. I could not synthesise the window — it needs the route table to be incomplete at t0 and complete at t1 with no intervening file event, and any file change I make to force it invalidates the very cache under test. My first attempt at a proof (moving the route dir aside and back) was inconclusive for exactly that reason, and I discarded it rather than count it as evidence.What is certain: the server was healthy and answering throughout, the deepest route never entered its table, and re-asking one constant url demonstrably cannot recover. A fresh url per poll is correct under either mechanism and cannot be worse than a constant one — but it is CI, over time, that confirms the flake is gone.
Draft because of that caveat.
🤖 Generated with Claude Code
https://claude.ai/code/session_019LhaMQFjLyhrrMxs1sxTZV