Skip to content

refactor: pre-emptive network interception refactor to ports/adapters #33919

@cacieprins

Description

@cacieprins

Network interception: ports & adapters refactor

This document is the program-level overview for the stacked ports-and-adapters refactor.


Goals

The HTTP/2 program needs Cypress to intercept and mutate network traffic without routing every request through the MITM proxy. Today, intercept logic, document injection, cookie simulation, and config policies are embedded in proxy and net-stubbing middleware.

This refactor applies a hexagonal architecture (aka, "ports and adapters) to network interception:

Goal How
Swap transport without rewriting intercept logic Pure NetworkInterceptionCore orchestrates matching, subscriptions, and policy phases; infrastructure lives behind driven ports
Keep cy.intercept stable Driver→server IPC unchanged at the boundary; ForInterceptRegistration adapter wraps legacy handlers
Configurator policies as data blockHosts, CSP allow-list, document rewrite register as NetworkPolicy objects, evaluated by NetworkPolicyRegistry
Incremental delivery 8 stacked PRs, each behavior-neutral, each mergeable independently
HTTP/2 readiness Explicit bypass at forwardToOrigin; stub ForBrowserNetworkAutomation for future CDP/BiDi adapters

Non-goals for this stack: changing user-facing APIs, replacing the proxy entirely, or implementing the HTTP/2 browser-automation path (that is a follow-on epic using the same core).


Architecture at a glance

Before (monolith)

Intercept matching, handler merge, outbound forwarding, HTML injection, cookie jar updates, and command-log emission all lived as inline logic inside @packages/proxy and @packages/net-stubbing middleware. Types were owned by net-stubbing. Server constructed NetworkProxy directly.

After (stage 7 — target)

┌─────────────────────────────────────────────────────────────────────────┐
│  @packages/server — composition root                                    │
│  createProxyRuntime()                                                   │
│    ├─ ConfiguratorNetworkPolicyAdapter  (driving port)                  │
│    ├─ registerDefaultNetworkPolicies()                                  │
│    └─ createDefaultNetworkInterceptionCore()                            │
│         └─ NetworkInterceptionCore + 6 proxy driven-port adapters       │
└─────────────────────────────────────────────────────────────────────────┘
         │                                    │
         ▼                                    ▼
┌─────────────────────┐            ┌──────────────────────────────────────┐
│ @packages/driver    │            │ @packages/proxy                      │
│ DriverIntercept…    │            │ defaultMiddleware → core delegates   │
│ DriverCommandLog…   │            │ Proxy*Adapter implementations        │
└─────────────────────┘            └──────────────────────────────────────┘
         │                                    │
         └──────────────┬─────────────────────┘
                        ▼
         ┌──────────────────────────────────────┐
         │ @packages/network-interception       │
         │  • Port interfaces (driving/driven)  │
         │  • NetworkInterceptionCore (pure+)   │
         │  • NetworkPolicyRegistry + policies  │
         │  • Shared types (NetEvent, routes)   │
         └──────────────────────────────────────┘
                        ▲
         ┌──────────────┴───────────────────────┐
         │ @packages/net-stubbing               │
         │  • Intercept I/O (body stream, subs) │
         │  • DriverInterceptRegistrationAdapter│
         └──────────────────────────────────────┘

Entry point: packages/server/lib/network-runtime.ts

Domain package: packages/network-interception/ (in early PRs, this is packages/network-policy)

Proxy adapters: packages/proxy/lib/adapters/


Port model

Driving ports (call into the core)

Who initiates work from outside the domain.

Port Package Adapter Responsibility
ForInterceptRegistration network-interception DriverInterceptRegistrationAdapter (net-stubbing) cy.intercept route add, subscribe, handler round-trips
ForNetworkPolicyRegistration network-interception ConfiguratorNetworkPolicyAdapter (server) Register config policies, run policy phases

Driven ports (core calls out to infrastructure)

Who performs I/O, browser hooks, and side effects.

Port Adapter(s) Responsibility
ForRequestInterception ProxyRequestInterceptionAdapter Pre-request correlation, forward to origin, blocked-host termination
ForResponseInterception ProxyResponseInterceptionAdapter Response body read, subscription handlers
ForDocumentPreparation ProxyDocumentPreparationAdapter Injection level, HTML/JS rewriter, security stripping
ForNetworkCapture ProxyNetworkCaptureAdapter Test Replay / protocol manager hooks
ForCookieState ProxyCookieStateAdapter Cookie jar attach + Set-Cookie capture
ForCommandLog ProxyCommandLogAdapter (server), DriverCommandLogAdapter (driver) Command log entries
ForBrowserNetworkAutomation (stub — HTTP/2 epic) CDP Fetch / BiDi network session hooks

Core orchestrator

NetworkInterceptionCore holds pure logic (route matching, subscription planning, handler merge, injection-level decisions, request-logging filter) and delegates I/O to injected driven ports. Middleware never calls adapters directly — it calls this.networkInterceptionCore.*.


System context (C4)

C4Context
  title Cypress network interception — system context
  Person(tester, "Test author", "Writes cy.intercept tests")
  System(cypress, "Cypress", "Test runner and proxy")
  System_Ext(aut, "Application under test", "Served at baseUrl")
  System_Ext(origin, "Third-party origins", "APIs, CDNs, assets")

  Rel(tester, cypress, "Runs tests")
  Rel(cypress, aut, "Automates browser")
  Rel(cypress, origin, "Proxied or CDP-intercepted HTTP")
Loading
C4Container
  title Monorepo containers — refactor boundary
  Person(tester, "Test author")
  Container_Boundary(cypress, "Cypress") {
    Container(driver, "@packages/driver", "cy.* commands")
    Container(server, "@packages/server", "HTTP server, sockets, composition root")
    Container(proxy, "@packages/proxy", "MITM proxy middleware")
    Container(netstub, "@packages/net-stubbing", "Intercept middleware + driver adapter")
    Container(ni, "@packages/network-interception", "Ports, core, policies, types")
    Container(socket, "@packages/socket", "Driver-server IPC")
    Container(rewriter, "@packages/rewriter", "HTML/JS source rewrite")
  }
  System_Ext(origin, "Origin servers")

  Rel(tester, driver, "Tests")
  Rel(driver, socket, "net events")
  Rel(socket, server, "WebSocket")
  Rel(server, proxy, "handleHttpRequest")
  Rel(proxy, netstub, "middleware stack")
  Rel(proxy, origin, "forwardToOrigin today")
  Rel(netstub, ni, "core + types")
  Rel(server, ni, "composition root")
  Rel(proxy, ni, "delegates through core")
  Rel(proxy, rewriter, "document injection")
Loading

Port / adapter topology (complete)

flowchart TB
  subgraph driving [Driving ports]
    FIR[ForInterceptRegistration]
    FNPR[ForNetworkPolicyRegistration]
  end

  subgraph driverPkg ["@packages/driver"]
    DCL[DriverCommandLogAdapter]
    CY[cy.intercept]
  end

  subgraph serverPkg ["@packages/server"]
    CPR[createProxyRuntime]
    CNPA[ConfiguratorNetworkPolicyAdapter]
    RDP[registerDefaultNetworkPolicies]
    CDNIC[createDefaultNetworkInterceptionCore]
  end

  subgraph corePkg ["@packages/network-interception"]
    NIC[NetworkInterceptionCore]
    NPR[NetworkPolicyRegistry]
    Pure[pure modules]
  end

  subgraph proxyPkg ["@packages/proxy/adapters"]
    PRI[ProxyRequestInterceptionAdapter]
    PRS[ProxyResponseInterceptionAdapter]
    PDP[ProxyDocumentPreparationAdapter]
    PNC[ProxyNetworkCaptureAdapter]
    PCS[ProxyCookieStateAdapter]
    PCL[ProxyCommandLogAdapter]
  end

  subgraph netstubPkg ["@packages/net-stubbing"]
    DIRA[DriverInterceptRegistrationAdapter]
    HIR[handleInterceptRequest/Response]
  end

  subgraph mw [Proxy middleware]
    REQ[request-middleware]
    RES[response-middleware]
  end

  CY -->|socket| FIR
  FIR --> DIRA --> HIR
  CPR --> CNPA --> NPR
  CPR --> RDP
  CPR --> CDNIC --> NIC
  CDNIC --> PRI & PRS & PDP & PNC & PCS & PCL
  CNPA -->|policyRegistration| NIC
  REQ & RES --> NIC
  NIC --> Pure
  NIC --> PRI & PRS & PDP & PNC & PCS & PCL
  DCL -.->|logInterception| FCL[ForCommandLog]
  PCL --> FCL
Loading

How HTTP/2 fits in

Two transport paths, one core

The refactor deliberately separates what happens to a request (match routes, run policies, fulfill, log) from how bytes move (MITM proxy vs browser network API).

flowchart TB
  subgraph today ["Today — proxy-default path (this stack)"]
    B1[Browser] --> P1[Cypress MITM proxy]
    P1 --> CORR[correlateBrowserPreRequest]
    CORR --> CORE1[NetworkInterceptionCore]
    CORE1 --> FWD[forwardToOrigin — Node HTTP]
    FWD --> O1[Origin]
  end

  subgraph future ["Future — HTTP/2 browser-automation path"]
    B2[Browser] --> CDP[CDP Fetch / BiDi network]
    CDP --> BNA[ForBrowserNetworkAutomation adapter]
    BNA --> CORE2[NetworkInterceptionCore — same core]
    CORE2 --> O2[Origin via browser stack]
  end
Loading

Key boundary: ForRequestInterception.forwardToOrigin sends the proxied request to the origin via Node HTTP. It is documented as the HTTP/2 bypass boundary — the browser-automation path must not call it; requests terminate at CDP Fetch instead of MITM forwarding.

Relevant code:

What stays the same across paths

Concern Owner Transport-agnostic?
Route matching, subscriptions, handler merge NetworkInterceptionCore Yes
Config policies (blockHosts, CSP, rewrite) NetworkPolicyRegistry Yes
cy.intercept registration ForInterceptRegistration Yes
Command log provenance ForCommandLog Yes
Pre-request correlation correlateBrowserPreRequest Proxy path today; BNA adapter future
Outbound HTTP forwardToOrigin Proxy only
Document injection / CSP strip ForDocumentPreparation Proxy path today; may move partially to BNA

HTTP/2 epic placeholders (not in this stack)

Artifact Purpose
ForBrowserNetworkAutomation Stub driven port for CDP/BiDi session hooks
createStubDrivenPorts() No-op adapters for HTTP/2 unit tests and alternate composition roots
NetworkInterceptionRuntime Facade so server can swap proxy-default vs browser-default runtime

Browser automation today (feeds the proxy path)

Even on the current proxy-default path, CDP/BiDi automation is involved upstream of the proxy:

  1. Browser emits a network request (CDP Network.requestWillBeSent or BiDi network.beforeRequestSent).
  2. Server records a browser pre-request and correlates it when the MITM proxy sees the same request (correlateBrowserPreRequest).
  3. Proxy middleware runs the intercept pipeline and eventually calls forwardToOrigin.
sequenceDiagram
  participant Browser
  participant CDP as CDP/BiDi automation
  participant Server
  participant Proxy
  participant Core as NetworkInterceptionCore
  participant Origin

  Browser->>CDP: requestWillBeSent
  CDP->>Server: addBrowserPreRequest
  Browser->>Proxy: MITM proxied request
  Proxy->>Core: correlateBrowserPreRequest
  Core->>Core: matchRoutes / handleRequest
  Core->>Proxy: forwardToOrigin
  Proxy->>Origin: Node HTTP
  Origin-->>Proxy: response
  Proxy->>Core: interceptResponse / document prep
Loading

On the HTTP/2 path, steps after correlation move into ForBrowserNetworkAutomation — the core's match/fulfill/policy logic stays, but forwardToOrigin drops out of the chain.


Stacked PR roadmap

Eight behavior-neutral PRs. Merge in order; each targets the previous branch as base.

Stage PR draft What it introduces
0 #33896 @packages/network-policy scaffold, types move, createProxyRuntime
1 #33912 ForInterceptRegistration + socket wiring
2 #33913 Policy registry + BlockedHosts registration
3 #33913 NetworkPolicyCore + intercept middleware delegation
4 #33914 Request/response driven ports + proxy adapters
5 #33915 Document preparation + CSP/rewrite policies
6 #33916 Capture, cookie, command-log adapters
7 #33918 Rename to @packages/network-interception, consolidate composition, wire BlockedHosts enforcement

Request lifecycle (proxy-default, post stage 7)

sequenceDiagram
  participant MW as Proxy middleware
  participant Core as NetworkInterceptionCore
  participant Pol as NetworkPolicyRegistry
  participant PRI as ProxyRequestInterceptionAdapter
  participant NS as net-stubbing I/O
  participant Origin

  MW->>Core: endRequestIfBlocked
  Core->>Pol: runPolicies request phase
  alt BlockedHosts matches
    Pol-->>MW: ctx.end 503
  else not blocked
    MW->>Core: correlateBrowserPreRequest
    Core->>PRI: correlate
    MW->>Core: handleRequest
    Core->>NS: handleInterceptRequest
    MW->>Core: forwardToOrigin
    Core->>PRI: sendRequestOutgoing
    PRI->>Origin: Node HTTP
    Origin-->>MW: response stream
    MW->>Core: interceptResponse
    Core->>NS: handleInterceptResponse
    MW->>Core: setInjectionLevel / injectHtml / ...
  end
Loading

Package dependency rules

@packages/network-interception
  ↑ (types, ports, core)
  ├── @packages/net-stubbing   (driver adapter, intercept I/O)
  ├── @packages/proxy          (proxy adapters, middleware delegates)
  ├── @packages/server         (composition root, configurator adapter)
  └── @packages/driver         (command-log adapter, port types)

@packages/network-interception must NOT import from proxy or net-stubbing
Adapters implement ports in the package that owns the legacy code

Verification

After stage 7 merges:

git diff refactor/ports-adapters-7 refactor/ports-adapters-backup
# expect empty (plus any CI-only commits on branch 0)

Full test gate:

yarn workspace @packages/network-interception test
yarn workspace @packages/proxy test
yarn workspace @packages/net-stubbing test
yarn workspace @packages/server test-unit
yarn check-ts

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions