Symptom
tests/e2e/app-router/rsc-fetch-errors.spec.ts:156 (redirect chain to a non-ok endpoint hard-navs to the post-redirect URL) times out on page.waitForURL("/about", { timeout: 10_000 }) and fails both initial run and Retry #1. PR #900 isn't touching app-router code, so the failure is unrelated to the diff under test.
Failing run: https://github.com/cloudflare/vinext/actions/runs/24939159481/job/73029827222?pr=900
Reproduction signal
Playwright trace shows the main frame navigated to /redirect-src three times in a row, never advancing to /about:
navigated to "http://localhost:4174/redirect-src"
navigated to "http://localhost:4174/redirect-src"
navigated to "http://localhost:4174/redirect-src"
TimeoutError: page.waitForURL: Timeout 10000ms exceeded.
The test's intercept chain depends on Playwright re-dispatching the route table on the 307 follow-up so both the /redirect-src.rsc (307 → /about.rsc) and the /about.rsc (500) handlers fire in sequence. The author already flagged this fragility in tests/e2e/app-router/rsc-fetch-errors.spec.ts:168-172:
Intercept chain depends on Playwright re-entering the route table on the 307 follow-up so both handlers fire in sequence (browser fetch defaults to redirect:follow). Migrating to a mocker that follows redirects without re-dispatching would silently skip the /about.rsc intercept and the test would fall through to the real backend.
The CI failure looks like exactly this scenario inverted: Playwright followed the 307 to /about.rsc itself without re-dispatching the route, so the 500 handler never fired and the navigation guard kept retrying the original /redirect-src request.
Hypotheses
- Playwright/Chromium version drift changed
route.fulfill 307 follow behaviour to internal-follow (no re-dispatch). Worth pinning the Playwright version and re-running the suite.
- Race between
page.goto("/"), hydration, and the intercept registration; if the intercept is replaced by the second registration before the first fetch lands, the 307 chain breaks. Less likely given page.route registers synchronously.
- The redirect handler returns a
Location header pointing at /about.rsc, but the request URL has a query string in some runs ((\?|$) regex match), which can desync the second intercept on the bare path.
Suggested fix direction
Replace the dual-intercept-and-pray pattern with an explicit redirect-follow strategy: a single page.route matcher that distinguishes the two URLs by inspecting request.url(), then returns 307 or 500 deterministically without depending on Playwright's route-redispatch semantics. Alternatively, route through a real test server that issues the 307 and 500 directly so the browser follows the chain without test-side mocking.
Severity
Blocks CI on unrelated PRs (e.g. #900). Should be quarantined or re-skipped until the redirect-chain mocking is reworked.
Symptom
tests/e2e/app-router/rsc-fetch-errors.spec.ts:156(redirect chain to a non-ok endpoint hard-navs to the post-redirect URL) times out onpage.waitForURL("/about", { timeout: 10_000 })and fails both initial run andRetry #1. PR #900 isn't touching app-router code, so the failure is unrelated to the diff under test.Failing run: https://github.com/cloudflare/vinext/actions/runs/24939159481/job/73029827222?pr=900
Reproduction signal
Playwright trace shows the main frame navigated to
/redirect-srcthree times in a row, never advancing to/about:The test's intercept chain depends on Playwright re-dispatching the route table on the 307 follow-up so both the
/redirect-src.rsc(307 →/about.rsc) and the/about.rsc(500) handlers fire in sequence. The author already flagged this fragility intests/e2e/app-router/rsc-fetch-errors.spec.ts:168-172:The CI failure looks like exactly this scenario inverted: Playwright followed the 307 to
/about.rscitself without re-dispatching the route, so the 500 handler never fired and the navigation guard kept retrying the original/redirect-srcrequest.Hypotheses
route.fulfill307 follow behaviour to internal-follow (no re-dispatch). Worth pinning the Playwright version and re-running the suite.page.goto("/"), hydration, and the intercept registration; if the intercept is replaced by the second registration before the first fetch lands, the 307 chain breaks. Less likely givenpage.routeregisters synchronously.Locationheader pointing at/about.rsc, but the request URL has a query string in some runs ((\?|$)regex match), which can desync the second intercept on the bare path.Suggested fix direction
Replace the dual-intercept-and-pray pattern with an explicit redirect-follow strategy: a single
page.routematcher that distinguishes the two URLs by inspectingrequest.url(), then returns 307 or 500 deterministically without depending on Playwright's route-redispatch semantics. Alternatively, route through a real test server that issues the 307 and 500 directly so the browser follows the chain without test-side mocking.Severity
Blocks CI on unrelated PRs (e.g. #900). Should be quarantined or re-skipped until the redirect-chain mocking is reworked.