Current behavior
In open mode on the browser network path, a site's own service worker can serve the site's content in place of Cypress's runner document. The user sees the site's 404 page where the Cypress app should be.
The mechanism: the browser network path serves the runner document on the origin under test. With baseUrl: https://app.example.com, the top frame is literally https://app.example.com/__/#/specs/runner?.... Many production sites register a service worker scoped at / (verified against several major ones). That scope entitles the worker to answer for /__/, which is Cypress's own document.
Two navigations hit this today:
| Navigation |
Trigger |
| Launch |
Open mode keeps a persistent browser profile. A worker registered in an earlier Cypress session survives. Clicking a spec navigates the top frame to the runner URL, and the leftover worker answers first. |
| Runner reload |
The user reloads the browser tab mid-session. The now-running worker answers for the reload. |
Interception cannot save the launch case: the navigation that cold-starts a worker pauses on no CDP session at all (established empirically in #34674 and #34757). Run mode is unaffected only because its fresh profile has no worker at first navigation — which is also why CI never caught this. The BROWSER_NETWORK_INTERCEPTION_ESCAPE tripwire fires, but it is a warning, not a recovery.
This is distinct from #34652 (closed). That issue was the navigation-preload escape. #34671 closed it; #34711 fixed the sibling ServiceWorkerAutoPreload escape (#34709). This issue is the sequel: the runner's own document, not the AUT's in-test requests.
Desired behavior
Cypress always serves its own runner document. An application service worker never answers for a Cypress-reserved path (/__/, /__cypress/, the cloud bundle namespaces), on any navigation, predicted or not.
Test code to reproduce
cypress open on Chrome with forceHttp1 unset and baseUrl set to the origin of any site that registers a root-scoped service worker (check with navigator.serviceWorker.getRegistrations() in the console — a registration whose scope is https://<host>/).
- Run any spec that visits the site, so its service worker registers. End the Cypress session.
- Reopen Cypress and click a spec: the site's 404 renders where the runner should be.
- Same effect without a relaunch: after step 2's spec runs, reload the browser tab.
A hostile minimal reproduction (root-scope worker answering /__/assets/e2e-poison* with a marker body) lives in system-tests/projects/e2e/static/framebusting/ as of PR #34762.
Cypress Version
16.0.0 (present at release; the 16.0.0 release notes do not mention it)
Debug Logs
Signature, not required for triage: a BROWSER_NETWORK_INTERCEPTION_ESCAPE warning naming a URL whose path starts with /__/. Under DEBUG=cypress:server:browsers:interception-escape-detector, the escape logs as a service-worker-served document with no Fetch pause.
Other
Fix is up in PR #34762 (two layers: instrumented workers decline runner-namespace requests; persisted worker state is cleared before the runner navigation). Workarounds until it ships in 16.0.1: use cypress run; or clear the browser profile / the site data for the AUT's origin; or set forceHttp1: true.
Attn Agents:
Machine-oriented section; humans see the summary above.
Mechanism. browserUrl = origin(baseUrl) + clientRoute (packages/config/src/utils.ts setUrls), so the runner document lives on the AUT origin under clientRoute (default /__/). A root-scope app SW is in scope for it. Cold-start navigations pause on no CDP session (harness data in #34757 body); cache-first workers answer from Cache API with no network request; so interception alone cannot protect the document.
Evidence. Standalone CDP harness, Chrome 152: a warm worker's DECLINED navigation (handler runs, no respondWith) falls through and pauses as a normal Fetch.requestPaused on the page session — fully interceptable. respondWith(fetch(e.request)) pauses on the worker session as XHR. Distinguishable and both safe. Cold-start and cache-first behavior independently re-verified on Chrome 152 (2/2 trials each); note that during a cold start a racing network fetch can reach the origin even though no Fetch.requestPaused fires anywhere — "no pause" is not "no packet". Negative result ruling out the rival design: Network.setBypassServiceWorker arm/clear (PR #34757) must enumerate every navigation path, cannot see user reloads, and a missed clear silently disables the app's worker mid-test — superseded, do not revive.
Fix design (PR #34762). (1) packages/proxy/lib/http/util/service-worker-injector.ts: wrapped fetch listeners decline paths under clientRoute, /${namespace}/, /__cypress-studio, /__cypress-cy-prompt (bare match mirroring isCloudBundleNamespace, packages/server/lib/adapters/internal-routes.ts), emitting fetchRequest {isControlled:false}; exemption JSON-embedded as the serialized IIFE's argument, threaded from MaybeInjectServiceWorker only when useBrowserNetworkInterception. (2) packages/server/lib/browsers/chrome.ts attachListeners: Storage.clearDataForOrigin {origin:'*', storageTypes:'service_workers,cache_storage'} on the page client before _navigateUsingCRI, gated on BrowserLaunchOpts.shouldClearPersistedServiceWorkers = browser-network path AND project-level testIsolation !== false.
Constraints. Injector file is Function.prototype.toString()-serialized: self only, no bare window/document/global/process/console (v8 snapshot doctor rewrites them into get_*() shims — silent, CI-only). SW script must stay byte-identical across update checks: serve cacheable + updateViaCache: 'all' in fixtures. MITM path (forceHttp1) is deliberately inert. testIsolation: false residual risk accepted: persisted never-instrumented worker + unpredicted navigation = one loudly-explained failure (tripwire copy names the remedy). cy-in-cy cannot host an e2e proof: with CYPRESS_INTERNAL_E2E_TESTING_SELF set, open_project.ts routes relaunches through connectToExisting, whose client uses fullyManageTabs: false; the Target.attachedToTarget listener that consumes worker attach events — and installs the __cypressServiceWorkerClientEvent binding — is registered only when fullyManageTabs is true, and the injected install handler waits for that binding unboundedly — no SW can install under cy-in-cy at all.
Fallback. None needed; both layers landed. If the decline rule ever regresses, the launch clear still covers persisted-profile launches, and forceHttp1: true remains the user-side escape.
Verification plan. Unit: packages/proxy/test/unit/http/util/service-worker-injector.spec.ts (executes the serialized string), chrome_spec.ts clear-before-navigate ordering pins, server-base_spec.ts escape-label predicate (mutation-verified). System: system-tests/test/proxy_disabled_framebusting_spec.ts — browser-network variant asserts the SW's marker body cannot come back for a reserved path, forceHttp1 variant asserts it does; red/green verified against the rule reverted.
Interim mitigation. cypress run; clear profile/site data; forceHttp1: true. Do not close this issue on the injector rule alone — the launch clear is what covers never-instrumented persisted workers.
Adjacent bugs surfaced by this work, not yet filed as issues. (a) An AUT request to a clientRoute path no Express route owns hangs forever on the browser network path — the loopback re-entry 404 never releases the outer CDP pause. (b) The injector's install-time binding wait has no timeout, wedging every registration in binding-less environments. (c) cy-in-cy's connectToExisting never consumes worker attach events, so no service worker can install there (blocks all SW e2e coverage under cy-in-cy).
Current behavior
In open mode on the browser network path, a site's own service worker can serve the site's content in place of Cypress's runner document. The user sees the site's 404 page where the Cypress app should be.
The mechanism: the browser network path serves the runner document on the origin under test. With
baseUrl: https://app.example.com, the top frame is literallyhttps://app.example.com/__/#/specs/runner?.... Many production sites register a service worker scoped at/(verified against several major ones). That scope entitles the worker to answer for/__/, which is Cypress's own document.Two navigations hit this today:
Interception cannot save the launch case: the navigation that cold-starts a worker pauses on no CDP session at all (established empirically in #34674 and #34757). Run mode is unaffected only because its fresh profile has no worker at first navigation — which is also why CI never caught this. The
BROWSER_NETWORK_INTERCEPTION_ESCAPEtripwire fires, but it is a warning, not a recovery.This is distinct from #34652 (closed). That issue was the navigation-preload escape. #34671 closed it; #34711 fixed the sibling
ServiceWorkerAutoPreloadescape (#34709). This issue is the sequel: the runner's own document, not the AUT's in-test requests.Desired behavior
Cypress always serves its own runner document. An application service worker never answers for a Cypress-reserved path (
/__/,/__cypress/, the cloud bundle namespaces), on any navigation, predicted or not.Test code to reproduce
cypress openon Chrome withforceHttp1unset andbaseUrlset to the origin of any site that registers a root-scoped service worker (check withnavigator.serviceWorker.getRegistrations()in the console — a registration whosescopeishttps://<host>/).A hostile minimal reproduction (root-scope worker answering
/__/assets/e2e-poison*with a marker body) lives insystem-tests/projects/e2e/static/framebusting/as of PR #34762.Cypress Version
16.0.0 (present at release; the 16.0.0 release notes do not mention it)
Debug Logs
Signature, not required for triage: a
BROWSER_NETWORK_INTERCEPTION_ESCAPEwarning naming a URL whose path starts with/__/. UnderDEBUG=cypress:server:browsers:interception-escape-detector, the escape logs as a service-worker-served document with no Fetch pause.Other
Fix is up in PR #34762 (two layers: instrumented workers decline runner-namespace requests; persisted worker state is cleared before the runner navigation). Workarounds until it ships in 16.0.1: use
cypress run; or clear the browser profile / the site data for the AUT's origin; or setforceHttp1: true.Attn Agents:
Machine-oriented section; humans see the summary above.
Mechanism.
browserUrl = origin(baseUrl) + clientRoute(packages/config/src/utils.tssetUrls), so the runner document lives on the AUT origin underclientRoute(default/__/). A root-scope app SW is in scope for it. Cold-start navigations pause on no CDP session (harness data in #34757 body); cache-first workers answer from Cache API with no network request; so interception alone cannot protect the document.Evidence. Standalone CDP harness, Chrome 152: a warm worker's DECLINED navigation (handler runs, no
respondWith) falls through and pauses as a normalFetch.requestPausedon the page session — fully interceptable.respondWith(fetch(e.request))pauses on the worker session as XHR. Distinguishable and both safe. Cold-start and cache-first behavior independently re-verified on Chrome 152 (2/2 trials each); note that during a cold start a racing network fetch can reach the origin even though noFetch.requestPausedfires anywhere — "no pause" is not "no packet". Negative result ruling out the rival design:Network.setBypassServiceWorkerarm/clear (PR #34757) must enumerate every navigation path, cannot see user reloads, and a missed clear silently disables the app's worker mid-test — superseded, do not revive.Fix design (PR #34762). (1)
packages/proxy/lib/http/util/service-worker-injector.ts: wrapped fetch listeners decline paths underclientRoute,/${namespace}/,/__cypress-studio,/__cypress-cy-prompt(bare match mirroringisCloudBundleNamespace,packages/server/lib/adapters/internal-routes.ts), emittingfetchRequest {isControlled:false}; exemption JSON-embedded as the serialized IIFE's argument, threaded fromMaybeInjectServiceWorkeronly whenuseBrowserNetworkInterception. (2)packages/server/lib/browsers/chrome.tsattachListeners:Storage.clearDataForOrigin {origin:'*', storageTypes:'service_workers,cache_storage'}on the page client before_navigateUsingCRI, gated onBrowserLaunchOpts.shouldClearPersistedServiceWorkers= browser-network path AND project-leveltestIsolation !== false.Constraints. Injector file is
Function.prototype.toString()-serialized:selfonly, no barewindow/document/global/process/console(v8 snapshot doctor rewrites them intoget_*()shims — silent, CI-only). SW script must stay byte-identical across update checks: serve cacheable +updateViaCache: 'all'in fixtures. MITM path (forceHttp1) is deliberately inert.testIsolation: falseresidual risk accepted: persisted never-instrumented worker + unpredicted navigation = one loudly-explained failure (tripwire copy names the remedy). cy-in-cy cannot host an e2e proof: withCYPRESS_INTERNAL_E2E_TESTING_SELFset,open_project.tsroutes relaunches throughconnectToExisting, whose client usesfullyManageTabs: false; theTarget.attachedToTargetlistener that consumes worker attach events — and installs the__cypressServiceWorkerClientEventbinding — is registered only whenfullyManageTabsis true, and the injected install handler waits for that binding unboundedly — no SW can install under cy-in-cy at all.Fallback. None needed; both layers landed. If the decline rule ever regresses, the launch clear still covers persisted-profile launches, and
forceHttp1: trueremains the user-side escape.Verification plan. Unit:
packages/proxy/test/unit/http/util/service-worker-injector.spec.ts(executes the serialized string),chrome_spec.tsclear-before-navigate ordering pins,server-base_spec.tsescape-label predicate (mutation-verified). System:system-tests/test/proxy_disabled_framebusting_spec.ts— browser-network variant asserts the SW's marker body cannot come back for a reserved path,forceHttp1variant asserts it does; red/green verified against the rule reverted.Interim mitigation.
cypress run; clear profile/site data;forceHttp1: true. Do not close this issue on the injector rule alone — the launch clear is what covers never-instrumented persisted workers.Adjacent bugs surfaced by this work, not yet filed as issues. (a) An AUT request to a
clientRoutepath no Express route owns hangs forever on the browser network path — the loopback re-entry 404 never releases the outer CDP pause. (b) The injector's install-time binding wait has no timeout, wedging every registration in binding-less environments. (c) cy-in-cy'sconnectToExistingnever consumes worker attach events, so no service worker can install there (blocks all SW e2e coverage under cy-in-cy).