[GH-ISSUE #901] Flaky E2E: rsc-fetch-errors.spec.ts "redirect chain to a non-ok endpoint hard-navs to the post-redirect URL" #199

Closed
opened 2026-05-06 12:38:06 +02:00 by BreizhHardware · 1 comment

Originally created by @NathanDrake2406 on GitHub (Apr 25, 2026).
Original GitHub issue: https://github.com/cloudflare/vinext/issues/901

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

  1. 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.
  2. 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.
  3. 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.

Originally created by @NathanDrake2406 on GitHub (Apr 25, 2026). Original GitHub issue: https://github.com/cloudflare/vinext/issues/901 ## 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 1. 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. 2. 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. 3. 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.
Author
Owner

@james-elicx commented on GitHub (Apr 27, 2026):

I don't believe this was fixed was it?

<!-- gh-comment-id:4324677565 --> @james-elicx commented on GitHub (Apr 27, 2026): I don't believe this was fixed was it?
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#199
No description provided.