Skip to content

fix(web-e2e): probe a fresh url each poll, and stop blaming next dev for answering#785

Merged
choraria merged 1 commit into
mainfrom
worktree-fix-e2e-probe-sticky-route
Jul 24, 2026
Merged

fix(web-e2e): probe a fresh url each poll, and stop blaming next dev for answering#785
choraria merged 1 commit into
mainfrom
worktree-fix-e2e-probe-sticky-route

Conversation

@choraria

Copy link
Copy Markdown
Contributor

Why

CI is red on main at 7d07827 in the web-e2e job. #783 touched only the IndexNow scripts and .gitleaksignoreapps/web and packages/ 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

✓ 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 "next dev did not answer … within 180000ms"

The server answered every probe, promptly, for the entire three minutes.

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. Every later poll for that same url came back in ~2ms of Next time, with no second Compiling line — 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. probeUrlFor now mints a fresh uuid per dynamic segment on every call, so each poll is 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 accepted as a parameter, so a caller cannot hoist it out of the poll loop and quietly restore the constant. The route (the src/app walk) is still hoisted; only the url is per-poll.

Second change: the timeout said next dev did not answer for 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-route unit tests 18/18. The new distinctness test is RED before this change — 20 calls produced 1 unique url (expected 1 to be 20).
  • apps/web e2e 25/25, run three times.
  • Root pnpm typecheck 33/33 (@webhook-co/web a fresh cache miss), pnpm lint 639 pass, prettier clean.

Honest caveat for the reviewer

The stickiness mechanism is inferred from the CI logs (single Compiling line, 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

…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
choraria marked this pull request as ready for review July 24, 2026 08:08
@choraria
choraria merged commit 2652fb1 into main Jul 24, 2026
30 checks passed
@choraria
choraria deleted the worktree-fix-e2e-probe-sticky-route branch July 24, 2026 08:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant