Cypress 网络拦截的传输无关内核:@packages/network-interception 的端口-适配器架构解析 Cypress 网络拦截的传输无关内核packages/network-interception 的端口-适配器架构解析【免费下载链接】cypressFast, easy and reliable testing for anything that runs in a browser.项目地址: https://gitcode.com/GitHub_Trending/cy/cypresspackages/network-interception是 Cypress 仓库中承载cy.intercept拦截“规则”层的核心包路由匹配、订阅规划、处理器合并、注入决策、请求日志策略与配置策略全部在这里实现却不沾染任何传输 I/O。本文以该包的 README.md 为骨架结合源码与测试逐层剖析其 hexagonal端口与适配器架构、NetworkInterceptionCore编排器、HttpIntercept洋葱中间件、策略注册表以及它在 MITM 代理与 CDP Fetch 两条传输上的复用方式读完你将对 Cypress 网络层“规则与传输解耦”的设计有一张完整地图。包的定位只装“规则”不碰字节流先看包的自我定义packages/network-interception/README.mdTransport-agnostic center for Cypress network interception:port interfaces, theNetworkInterceptionCoreorchestrator, thepolicy registry, and the shared types behindcy.intercept.这个包存放的是拦截的规则——路由匹配route matching、订阅规划subscription planning、处理器结果合并handler merge、注入级别与请求日志决策、配置策略。它不持有任何 I/O。凡是传输相关的东西MITM 代理中间件、CDP Fetch、driver IPC、cookie jar、Test Replay 抓取都躲在接口port后面由组合根composition root注入。包内依赖也刻意收敛从 package.json 可见其运行时依赖仅有packages/types、debug、lodash与minimatch并被明确约束「不得 importpackages/proxy或packages/net-stubbing」——依赖箭头只向内指。唯一的双向“外交”通道就是packages/types中的共享类型其中包含RouteMatcher等公共类型这在架构上保证了传输实现永远无法反向侵入内核。为什么引入端口与适配器规则跑在两条传输上在重构之前拦截逻辑内联在packages/proxy和packages/net-stubbing的中间件里与 MITM 代理硬绑定。问题在于Cypress 如今要在两条不同的传输上跑同一套匹配、处理器与策略行为——HTTP/1 MITM 代理以及浏览器内基于 CDP Fetch 的路径。既然行为一致而字节流移动方式不同规则就不能再依赖“字节怎么搬”。解决方案是经典的六边形架构端口与适配器ports and adapters把规则放在传输无关的中心把 I/O 隔离在接口背后为每条传输注入不同的实现。README 给出一张术语映射表Hex 术语角色本 monorepo 中的体现Port端口拦截“内部”边缘的契约lib/ports/下的For*类型Adapter适配器委托给传输专属代码来实现端口packages/*/lib/adapters/下的*Adapter类Driving port主动端口/主端口外部参与者调用进入拦截逻辑ForInterceptRegistration、ForNetworkPolicyRegistrationDriven port被动端口/副端口拦截逻辑向外调用I/OForRequestInterception、ForCookieState等Core核心领域编排不 import 任何传输NetworkInterceptionCorelib/core/Composition root组合根构造并注入适配器与核心createProxyRuntime()/createCdpFetchRuntime()packages/server/lib/network-runtime.ts一句话记忆driving port 是别人打给我们的电话driven port 是我们打给别人的电话。cy.intercept注册请求从 driver 进来driving核心要落 cookie、写命令日志、上报抓包时则打出去driven。包内布局纯粹的核心 一切其余lib/core/—— 无副作用的内核模块职责route-matching.tsmatchRoutes、doesRouteMatch、getMatchableForRequest、CORS 预检匹配matcher-fields.ts判定RouteMatcher哪些字段是字符串匹配器plan-subscriptions.ts每个被匹配路由订阅哪些事件merge-handler-result.ts把 driver 处理器结果合并回请求/响应document-preparation.ts注入级别与反 frame-busting 移除决策共享的 content-type /Accept/ service-worker 谓词request-logging.ts决定请求是否出现在命令日志http-intercept.tsHttpIntercept—— 传输无关的中间件洋葱network-interception-core.tsNetworkInterceptionCore—— 把决策委托给纯模块、把 I/O 委托给被动端口lib/core/index.ts将这些模块统一 re-export构成内核的公共出口。其余部分路径内容lib/ports/driving-ports.tsForInterceptRegistration、ForNetworkPolicyRegistrationlib/ports/driven-ports.ts六个 I/O 端口 ForBrowserNetworkAutomation占位桩lib/ports/http-interception.tsHttpRequest/HttpResponse、TransportCodecPort、InterceptMiddlewarelib/registry/NetworkPolicyRegistry—— 默认的ForNetworkPolicyRegistration实现lib/policies/NetworkPolicy形态、配置策略工厂、registerDefaultNetworkPolicies()lib/types/external-types.ts公共 API。被 cli/scripts/sync-typedefs.ts 逐字拷贝到cli/types/net-stubbing.d.ts—— 改动会随版本发布给所有用户lib/types/internal-types.tsNetEventdriver↔server 协议、可序列化属性清单lib/runtime.tsNetworkInterceptionRuntime门面由 proxy runtime 实现端口与适配器全景Driving主动端口—— 外部调进来PortAdapter所在包ForInterceptRegistrationDriverInterceptRegistrationAdapternet-stubbingForNetworkPolicyRegistrationConfiguratorNetworkPolicyAdapterserverForInterceptRegistration的契约driving-ports.ts只暴露一个handleEvent(request)事件名是route:added/subscribe/event:handler:resolved/send:static:response四者之一——这正是 driver 侧cy.intercept注册与 handler 往返的 IPC 通道。Driven被动端口—— 核心向外调用PortAdapter(s)职责ForRequestInterceptionProxyRequestInterceptionAdapter请求前关联pre-request correlation、被拦截主机终止ForResponseInterceptionProxyResponseInterceptionAdapter响应拦截续延ForDocumentPreparationProxyDocumentPreparationAdapter注入级别、HTML 注入、安全头剥离ForNetworkCaptureProxyNetworkCaptureAdapterTest Replay / 协议抓取ForCookieStateProxyCookieStateAdaptercookie jar 挂接与Set-Cookie捕获ForCommandLogProxyCommandLogAdapter、DriverCommandLogAdapter命令日志条目ForBrowserNetworkAutomation无 —— 见 Known gaps预留浏览器网络会话钩子中间件从不直接调适配器而是调用this.networkInterceptionCore.*由核心负责路由到被注入的端口。以 cookie 为例ForCookieState 只有attachCrossOriginCookies与copyCookiesFromResponse两个方法恰对应“出站挂 cookie / 回包收Set-Cookie”两个时机。核心编排器NetworkInterceptionCore 一窥network-interception-core.ts 是纯粹的门面式编排器构造函数接收最多七个可选端口L20-L29所有方法要么转发到lib/core/下的纯函数要么转发到某个 driven port。例如matchRoutes()/matchesRoutePreflight()/mergeIncomingRequestChanges()直接调纯模块endRequestIfBlocked(ctx)L63-L73把「运行请求期策略」这一函数闭包传给requestInterception端口runRequestPolicies(ctx)L85-L103从 ctx 中抽出{ url, method, requestId }构成NetworkExchange并以phase: request调用策略注册端口的runPoliciesinterceptResponse、setInjectionLevel、injectHtml、removeSecurity、attachCrossOriginCookies、notifyResponseStreamReceived等则是对应 driven port 的一对一薄转发。注意一个细节每当对应的端口未配置时核心会抛出形如NetworkInterceptionCore.requestInterception is not configured的错误——端口是可选的构造依赖这允许核心在只做纯匹配不接 I/O的场景下被轻量实例化也强制组合根显式声明“这条传输支持什么”。路由匹配字符串、布尔、数字三类字段的归一化比较路由规则的正确性是cy.intercept的灵魂route-matching.ts 有大量值得细读的工程细节doesRouteMatchL19-L94把 matcher 字段按类型拆成三类字符串匹配字段URL、method、headers、auth……由 matcher-fields.ts 提供清单、布尔字段https、数值字段port。字符串比较支持value matcher、minimatch(value, matcher, { matchBase: true })的 glob以及正则对象matcher.testmethod 比较前统一转小写。getMatchableForRequestL96-L135从authorization头中解析 Basic 认证严格按 RFC 7617 只切第一个冒号、拒绝无分隔符的畸形凭据并借用url.parse拆出hostname/path/pathname/port/queryport被强转为数值无显式端口时按协议回落 443/80。matchRoutesL140-L154把路由分成 middleware 与 handler 两组middleware 在前、handler 逆序在后后者注册越晚优先级越高过滤掉disabled的路由。matchesRoutePreflightL172-L203判定代理是否应自动应答 CORS 预检OPTIONS当请求携带access-control-request-method头、忽略 method/headers/auth 后有路由命中、且没有路由显式声明处理OPTIONS时返回 true。HttpIntercept无传输的中间件洋葱HttpInterceptTRequest, TResponsehttp-intercept.ts把“拦截”实现成与具体传输解耦的洋葱。核心逻辑只有use与handleuse (middleware: InterceptMiddleware): void // 压栈中间件handle的流程L21-L56是先用 codecdecodeRequest把传输原生请求解码为中性HttpRequest→ 用reduceRight把中间件数组层层包成单个forward函数 → 依次穿透中间件最内层terminal把最终HttpRequest用 codecencodeRequest还原成传输请求、调用传输的next、再把返回的传输响应decodeResponse回中性形态 → 最后经encodeResponse编码输出并在finally中调用可选的codec.releaseRequest?.(id)释放请求。codec 就是洋葱与字节流之间的唯一接口。TransportCodecPort契约http-interception.ts共五个方法encodeRequest/decodeRequest/encodeResponse/decodeResponse外加可选的releaseRequest。中性HttpRequest/HttpResponse形态只含id、url、method、headers、body、resourceType等字段其中HttpResponse.captureStream的注释点明了一个精妙设计它承载“浏览器实际交付的字节流”CDP 路径解码后的真实流 / stub 响应的 fulfilled 字节仅供 Test Replay 消费与喂养中间件 body 路径的bodyStream刻意区分——避免 Test Replay 的消费破坏中间件侧对流的完整可消费性。一个核心两条传输MITM 代理与 CDP FetchCypress 拦截浏览器流量有两条路径HTTP/1 MITM 代理或浏览器内通过 CDP Fetch。选择逻辑在isBrowserNetworkMode()packages/server/lib/util/network-mode.tsChromium 系浏览器默认走 CDP 路径forceHttp1、Firefox、WebKit 与 Electron 则留在代理上。两条路径跑同一个HttpIntercept中间件洋葱、同一套核心差异只在codec——每种传输一份的TransportCodecPort实现。README 明确区分了两种 codec 角色角色用途Transport codec传给new HttpIntercept(codec)适配传输塞进来的原生请求/响应Pipeline codec传给createLegacyProxyPipeline(codec)构建 legacy 中间件上下文各拦截对应的 codec拦截Transport codecPipeline codecExpress / MITM 代理networkProxy.codec—— http-codec.tsproxy同左CDP FetchcreateCdpFetchCodec()—— cdp-fetch-codec.tsservercreateSyntheticProxyCodec()—— synthetic-proxy-codec.tsproxy在浏览器网络模式下两个拦截同时存在CDP 那个处理浏览器流量Express 那个仍服务内部路由与 studio / cy-prompt 转发二者共享中间件阶段但保持为两个独立拦截。关键工程收益在 README 末尾点透因为 CDP 路径通过 synthetic Express 上下文驱动NetworkProxylegacy 中间件栈cookies、blocked hosts、rewriter、net-stubbing在两条传输上无需改动即可原样运行——这正是把兼容面收敛到 codec 一处所换来的复用红利。组合根两条 runtime 的同一条脊柱两个组合根都位于 packages/server/lib/network-runtime.ts由 server-base.ts 依据网络模式选择。二者共享同一脊柱ConfiguratorNetworkPolicyAdapter (driving port) registerDefaultNetworkPolicies() createProxyNetworkInterception() (core 6 driven-port adapters) NetworkProxy (legacy middleware stack) HttpIntercept(codec) (middleware onion)createProxyRuntime()基于 proxy codec 构建一个拦截返回NetworkInterceptionRuntimecreateCdpFetchRuntime()构建上述两个拦截外加一个CdpFetchTransport并额外提供start/stop/attachExtraTarget用于 popup 与 service-worker 会话。策略注册表配置派生规则如何落地从项目配置派生的规则——blockHosts、experimentalCspAllowList、modifyObstructiveCode——在启动时由registerDefaultNetworkPolicies()register-default-network-policies.ts注册为NetworkPolicy对象随后由NetworkPolicyRegistry求值。一个NetworkPolicypolicies/types.ts的形态是五元组name、provenanceconfig | test、phasesrequest | response | error、谓词when(exchange)与处理器apply(ctx)。策略apply时上下文PolicyContext携带phase、exchange与可变的state结果袋并提供continue()API 对称的无操作与end()置 ended 标志。注册默认策略的工厂调用清晰可见L29-L41blockHosts、cspAllowList、documentRewrite三者依次add进注册表。NetworkPolicyRegistry.runPoliciesnetwork-policy-registry.ts是一个线性短路链遍历策略列表跳过不包含当前phase或when()不命中的策略对命中的调用apply(ctx)一旦某个策略end()就立即返回{ ended: true, state }。请求期策略是活的代理请求中间件调用core.endRequestIfBlocked()→ 核心以phase: request跑注册表 → 调用ctx.end()的策略截断链路 →blocked-hosts策略把命中结果写进ctx.state供适配器转成 HTTP 503 响应。这解释了 Cypress 中blockHosts配置为何能干净利落地让被禁主机得到 503决策是策略翻译成状态码是适配器的活。已知缺口诚实的边界声明README 用一整节记录当前实现的未完成边界这些内容对理解代码极具价值响应期策略从不执行。runPolicies只以phase: request被调用因此csp-allow-list与document-rewrite虽已注册但不可达它们的apply是 no-op。CSP 白名单与文档改写仍直接在代理响应中间件中强制完成error相位既无策略也无调用方。可以对照 register-default-network-policies.ts 中三个策略的注册与核心 runRequestPolicies 固定写死phase: request来互相印证。ForBrowserNetworkAutomation是空桩。它最初为浏览器网络路径预留但该路径最终通过TransportCodecPort缝打通因此无人实现也无人注入。forwardToOrigin不可达。端口、核心方法、ProxyRequestInterceptionAdapter.forwardToOrigin俱已就位但代理在 http-codec.ts 中直接调用sendRequestOutgoing而非经由核心形成了一条绕过核心的死路。公共类型是“对外发布面”lib/types/external-types.ts被 README 郑重标注为Public API它由 cli/scripts/sync-typedefs.ts 逐字拷贝到cli/types/net-stubbing.d.ts。也就是说包内这一处源码改动会以类型定义的形式随 Cypress 发布流向每一位使用cy.intercept类型提示的用户。这解释了为何NetEvent等 driver↔server 协议类型被单独放进 internal-types.ts含可序列化属性清单——公共面与内部协议面被刻意切开。测试与开发包自身及其相关包用 vitest/yarn workspace 驱动README 给出的命令如下yarn workspace packages/network-interception test yarn workspace packages/net-stubbing test yarn workspace packages/proxy test yarn workspace packages/server test-unit -- network-runtime_speccy.intercept的行为级覆盖不在本包内而在 packages/driver/cypress/e2e/commands/net_stubbing.cy.ts——它作为 Cypress 自身的端到端测试以真实浏览器会话验证 driver 侧 API 到服务端拦截内核的完整链路。本包的 test/unit 则是针对纯内核与策略的单元测试。延伸阅读适配器侧的实现笔记packages/net-stubbing/lib/adapters/README.mddriver 侧与 packages/server/lib/adapters/README.mdserver 侧组合根packages/server/lib/network-runtime.ts网络模式选择packages/server/lib/util/network-mode.tsCDP 侧 codecpackages/server/lib/browsers/cdp-protocol/cdp-fetch-codec.ts类型同步脚本cli/scripts/sync-typedefs.ts。小结packages/network-interception是理解 Cypress 网络层的一把钥匙它示范了在“同一行为、两套字节搬运工”的约束下如何用端口与适配器把规则抽成纯领域、把 I/O 钉在接口之后、在组合根里完成装配。无论是想深入cy.intercept的路由匹配语义、CORS 预检自动应答逻辑还是想搞清blockHosts为何产出 503、以及两条传输为何能共享 legacy 中间件栈都可以从这份端口-适配器地图出发逐文件顺藤摸瓜。【免费下载链接】cypressFast, easy and reliable testing for anything that runs in a browser.项目地址: https://gitcode.com/GitHub_Trending/cy/cypress创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考