[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-strapi-article:zh:container-build-runtime-proxy-validation":3,"blog-article:\u002Fblog\u002Fcontainer-build-runtime-proxy-validation":1217,"blog-related:\u002Fblog\u002Fcontainer-build-runtime-proxy-validation":1218,"blog-translations:\u002Fblog\u002Fcontainer-build-runtime-proxy-validation":1219},{"available":4,"article":5,"relatedArticles":260,"translations":1043},true,{"source":6,"id":7,"documentId":8,"translationKey":8,"locale":9,"slug":10,"path":11,"_path":11,"title":12,"description":13,"excerpt":13,"image":14,"publishedAt":15,"updatedAt":16,"author":17,"tags":18,"featured":19,"sortOrder":20,"readingTime":21,"bodyType":22,"bodyBlocks":23,"bodyHtml":259},"strapi",3929,"u4wlg870wzusl7aan4m9xg0h","zh","container-build-runtime-proxy-validation","\u002Fblog\u002Fcontainer-build-runtime-proxy-validation","容器构建与运行阶段：代理IP 配置该如何分别验证","构建阶段的代理和容器运行期的代理，读配置的进程不同、网络路径也不同，很多「构建能通、运行连不上」就出在这里。文章对照构建参数、docker run 与编排文件三种注入方式，分别给出观测点和判定，最后用一张两阶段证据表收口。","https:\u002F\u002Fcms.quanqiudaili.com\u002Fuploads\u002Flarge_cover_b3a0045145.webp","2026-11-04T06:47:58.261Z","2026-11-04T06:47:58.248Z","Global Proxies Team",[],false,0,4,"blocks",[24,30,36,49,53,57,61,73,77,81,87,105,109,125,129,133,137,157,161,165,179,183,194,198,216,220,224,228,235,239,245,249,255],{"type":25,"children":26},"paragraph",[27],{"text":28,"type":29},"同一个镜像的构建和运行由不同进程发起外部请求，代理配置和网络路径需要分别验证。基础镜像拉取、构建步骤中的依赖下载，以及容器内应用请求可能经过不同出口；不能仅凭“容器能联网”推断它们都使用了预期代理。","text",{"type":31,"level":32,"children":33},"heading",2,[34],{"text":35,"type":29},"构建阶段和运行阶段为什么要分别验证",{"type":37,"format":38,"children":39},"list","unordered",[40,45],{"type":41,"children":42},"list-item",[43],{"text":44,"type":29},"构建期包含 builder\u002Fdaemon 拉取基础镜像，以及构建步骤中的 `RUN apt-get`、`pip install`、`npm ci` 等网络请求；两者可能走不同配置和路径，需分开核对。",{"type":41,"children":46},[47],{"text":48,"type":29},"运行期请求由容器内的应用进程发起。容器重启后环境变量仍在，但连接会重新建立。",{"type":25,"children":50},[51],{"text":52,"type":29},"两条路径的共同点是都会受宿主网络、DNS 和出口影响；不同点在于配置来源和验证手段。构建期代理可通过 Docker 预定义代理参数或 builder\u002Fdaemon 配置提供；运行期代理来自环境变量、编排文件或应用自身配置。把它们混在一起验证，最常见的结果是\"构建能过、运行超时\"，或者反过来。",{"type":31,"level":32,"children":54},[55],{"text":56,"type":29},"构建镜像时如何把代理配置传进容器",{"type":25,"children":58},[59],{"text":60,"type":29},"不要把含凭据的代理 URL 写进 Dockerfile。Docker 的 `HTTP_PROXY`、`HTTPS_PROXY` 等是预定义代理构建参数，通常通过构建命令或 Docker 客户端配置传入，无需在 Dockerfile 声明或引用。官方文档说明这些预定义参数有专门的缓存与历史处理规则，避免自行复制到普通 `ARG`。",{"type":25,"children":62},[63,65,71],{"text":64,"type":29},"基础镜像拉取由 builder\u002Fdaemon 路径处理，需和构建步骤中的下载分开核验。具体行为以实际 Docker\u002Fbuildx 版本为准，参见 ",{"text":66,"type":29,"marks":67},"Docker proxy arguments",[68],{"href":69,"type":70},"https:\u002F\u002Fdocs.docker.com\u002Fbuild\u002Fbuilding\u002Fvariables\u002F#proxy-arguments","link",{"text":72,"type":29},"。不要通过 `env` 原样输出代理变量，以免构建日志记录凭据。",{"type":31,"level":32,"children":74},[75],{"text":76,"type":29},"如何确认镜像构建真的通过代理下载依赖",{"type":25,"children":78},[79],{"text":80,"type":29},"构建日志能证明命令执行了，不能单独证明依赖下载走了代理。记录 Docker\u002Fbuildx 版本、builder 类型及缓存状态；缓存命中时构建步骤可能没有发出网络请求。用受控依赖请求和代理侧或目标服务日志核验实际到达。",{"type":37,"format":38,"children":82},[83],{"type":41,"children":84},[85],{"text":86,"type":29},"记录 Docker\u002Fbuildx 版本、builder 类型和相关缓存是否命中。缓存命中时该步骤可能没有发出网络请求。",{"type":37,"format":38,"children":88},[89,93,97,101],{"type":41,"children":90},[91],{"text":92,"type":29},"观测点：脱敏配置检查 ｜ 构建期含义：变量是否存在及来源 ｜ 通过标准：仅显示存在性、脱敏主机\u002F端口，不输出凭据",{"type":41,"children":94},[95],{"text":96,"type":29},"观测点：受控依赖请求 ｜ 构建期含义：包管理器是否实际发出请求 ｜ 通过标准：代理或目标侧日志能关联到请求",{"type":41,"children":98},[99],{"text":100,"type":29},"观测点：缓存状态 ｜ 构建期含义：是否真实执行下载 ｜ 通过标准：明确标记命中\u002F未命中缓存",{"type":41,"children":102},[103],{"text":104,"type":29},"观测点：builder 信息 ｜ 构建期含义：结果适用范围 ｜ 通过标准：记录 Docker\u002Fbuildx 版本与 builder 类型",{"type":25,"children":106},[107],{"text":108,"type":29},"缓存会影响是否发生网络请求。验证前确认相关构建层及包管理器缓存状态；必要时使用干净测试缓存或不同测试依赖，避免未实际下载却将构建成功当作代理生效证据。",{"type":25,"children":110},[111,113,116,118,123],{"text":112,"type":29},"构建参数行为见 ",{"text":66,"type":29,"marks":114},[115],{"href":69,"type":70},{"text":117,"type":29},"；运行期参数配置可对照",{"text":119,"type":29,"marks":120},"代理设置帮助",[121],{"href":122,"type":70},"https:\u002F\u002Fhelp.quanqiudaili.com\u002Fdocs\u002Fproxy-Settings",{"text":124,"type":29},"。",{"type":31,"level":32,"children":126},[127],{"text":128,"type":29},"容器运行时如何确认代理配置来自哪里",{"type":25,"children":130},[131],{"text":132,"type":29},"运行期代理可来自命令行、编排文件、镜像环境或应用配置。变量进入容器不代表每个进程都会读取；逐个副本检查配置来源，并用目标日志、代理到达记录和应用实际出口互证。检查时仅输出变量存在性及脱敏后的主机、端口，绝不打印凭据。",{"type":25,"children":134},[135],{"text":136,"type":29},"变量存在不等于生效。语言和库的默认行为不同，必须按实际技术栈验证：",{"type":37,"format":38,"children":138},[139,143,153],{"type":41,"children":140},[141],{"text":142,"type":29},"Python 的 requests 常见版本默认 `trust_env=True`，会读环境变量，具体以版本为准。",{"type":41,"children":144},[145,147,152],{"text":146,"type":29},"Axios 在 Node.js 的适用 HTTP adapter 中可读取代理环境变量；`proxy: false` 可禁用 Axios 的代理处理。Fetch、定制 adapter 或 agent 的行为应单独验证，记录 Axios 版本、adapter 和最终出口。参见 ",{"text":148,"type":29,"marks":149},"Axios request config",[150],{"href":151,"type":70},"https:\u002F\u002Faxios-http.com\u002Fdocs\u002Freq_config",{"text":124,"type":29},{"type":41,"children":154},[155],{"text":156,"type":29},"Go 的 `http.ProxyFromEnvironment` 从进程环境解析代理配置，需按所用 Go 版本和客户端行为验证实际请求。",{"type":31,"level":32,"children":158},[159],{"text":160,"type":29},"如何区分容器网络和宿主机网络造成的问题",{"type":25,"children":162},[163],{"text":164,"type":29},"容器里的 `127.0.0.1` 指向容器自身，不是宿主机。代理跑在宿主机上时：",{"type":37,"format":38,"children":166},[167,171,175],{"type":41,"children":168},[169],{"text":170,"type":29},"bridge 网络：容器访问宿主机要用 `host.docker.internal`（Docker Desktop）或网桥网关 IP，不能写 `127.0.0.1`。",{"type":41,"children":172},[173],{"text":174,"type":29},"`--network host`：容器与宿主机共享网络命名空间，此时 `127.0.0.1` 才指向宿主机。",{"type":41,"children":176},[177],{"text":178,"type":29},"代理在另一个容器里：用 Compose 服务名或自定义网络内的容器 IP。",{"type":25,"children":180},[181],{"text":182,"type":29},"这个区别解释了最常见的\"构建时能通、运行时连不上\"：构建阶段的基础镜像拉取发生在守护进程侧，`RUN` 内下载发生在构建容器里，网络路径和容器内应用完全不同。",{"type":25,"children":184},[185,187,192],{"text":186,"type":29},"容器网络连接问题可用",{"text":188,"type":29,"marks":189},"智能检测助手",[190],{"href":191,"type":70},"https:\u002F\u002Fhelp.quanqiudaili.com\u002Fdocs\u002FSmart-check",{"text":193,"type":29},"辅助排查，但最终仍需用应用请求和代理\u002F目标记录互证。",{"type":31,"level":32,"children":195},[196],{"text":197,"type":29},"用什么证据验收构建期和运行期代理",{"type":37,"format":38,"children":199},[200,204,208,212],{"type":41,"children":201},[202],{"text":203,"type":29},"阶段：构建期 ｜ 注入方式：Docker 预定义代理参数 \u002F 客户端配置 ｜ 生效范围：builder 支持的构建步骤 ｜ 观测点：受控依赖请求及代理\u002F目标到达记录 ｜ 判定：验证具体下载是否经过预期路径",{"type":41,"children":205},[206],{"text":207,"type":29},"阶段：运行期 ｜ 注入方式：`docker run -e` \u002F 编排配置 ｜ 生效范围：变量可见于容器进程；是否使用取决于应用库 ｜ 观测点：脱敏配置、目标日志和实际请求出口 ｜ 判定：按应用及副本逐个验证",{"type":41,"children":209},[210],{"text":211,"type":29},"阶段：运行期 ｜ 注入方式：编排文件 `environment` ｜ 生效范围：服务内所有副本 ｜ 观测点：副本日志、出口 IP ｜ 判定：与预期一致",{"type":41,"children":213},[214],{"text":215,"type":29},"阶段：宿主机代理 ｜ 注入方式：网关 IP \u002F `host.docker.internal` ｜ 生效范围：依赖网络模式 ｜ 观测点：连接测试、抓包 ｜ 判定：能连到代理端口",{"type":25,"children":217},[218],{"text":219,"type":29},"每个阶段分别保存注入来源、具体请求进程、缓存状态及实际请求到达证据。变量进入进程不代表应用读取；只有受控下载或业务请求与代理\u002F目标记录能够关联，才支持“该请求经代理”的结论。",{"type":25,"children":221},[222],{"text":223,"type":29},"验收时按阶段分别判定：构建期核对具体依赖下载的代理\u002F目标到达记录；运行期核对实际应用请求及具体副本的路径。任一阶段缺证据，就补测该阶段，不要用另一阶段的结果替代。",{"type":31,"level":32,"children":225},[226],{"text":227,"type":29},"FAQ",{"type":25,"children":229},[230],{"text":231,"type":29,"marks":232},"Q：可以在 Dockerfile 里直接写 `ENV HTTP_PROXY` 吗？",[233],{"type":234},"bold",{"type":25,"children":236},[237],{"text":238,"type":29},"A：不要把含凭据的代理 URL 写入 Dockerfile 或镜像。优先使用 Docker 支持的预定义代理参数或受控秘密注入，并检查日志、构建缓存和最终镜像；多阶段构建本身不能证明凭据已清理。",{"type":25,"children":240},[241],{"text":242,"type":29,"marks":243},"Q：构建成功但运行时报代理连接失败，先查什么？",[244],{"type":234},{"type":25,"children":246},[247],{"text":248,"type":29},"A：先确认运行期实际使用的代理IP可达、地区与预期一致，再查网络路径。宿主机代理在 bridge 网络下无法通过 `127.0.0.1` 访问，换成网关 IP 或 `host.docker.internal` 再试。",{"type":25,"children":250},[251],{"text":252,"type":29,"marks":253},"Q：怎么确认运行期用的是我设置的代理，而不是镜像里自带的？",[254],{"type":234},{"type":25,"children":256},[257],{"text":258,"type":29},"A：先只检查脱敏后的变量来源，再通过应用真实请求的目标日志或代理到达记录确认。变量存在不代表应用会读取；不要打印完整环境变量或代理凭据。","",[261,500,771],{"source":6,"id":262,"documentId":263,"translationKey":263,"locale":9,"slug":264,"path":265,"_path":265,"title":266,"description":267,"excerpt":267,"image":268,"publishedAt":269,"updatedAt":270,"author":17,"tags":271,"featured":19,"sortOrder":20,"readingTime":272,"bodyType":22,"bodyBlocks":273,"bodyHtml":259},3937,"eg98712cpvp1nwo9plj025g4","websocket-reconnection-session-recovery-testing","\u002Fblog\u002Fwebsocket-reconnection-session-recovery-testing","WebSocket 断线重连：代理IP 出口切换后如何验证会话恢复","客户端显示已重连，用户却丢了消息或看到重复下单，是实时应用常见的假恢复。文章按客户端正常关闭、网络中断与服务端重启三类断线分别制造中断，比较恢复耗时、丢失与重复条数，再用消息序号、去重窗口和业务状态核对，确认恢复的是业务而不只是连接。","https:\u002F\u002Fcms.quanqiudaili.com\u002Fuploads\u002Flarge_Web_Socket_cover_2052233c88.webp","2026-11-04T06:50:15.056Z","2026-11-04T06:50:15.027Z",[],3,[274,278,282,292,296,300,304,308,312,321,325,347,351,355,369,373,377,381,399,403,407,411,415,419,423,427,441,445,449,457,467,470,476,480,486,490,496],{"type":25,"children":275},[276],{"text":277,"type":29},"日志里出现\"重连成功\"，并不代表业务恢复完成：消息可能缺失、重复处理或状态未同步。应按受控断线场景检查重连与业务状态，并根据协议契约验证恢复位置。本篇不预设断线一定发生在代理出口，也不把单一关闭码当作根因。",{"type":31,"level":32,"children":279},[280],{"text":281,"type":29},"怎样判断连接恢复是否已经变成业务恢复",{"type":37,"format":38,"children":283},[284,288],{"type":41,"children":285},[286],{"text":287,"type":29},"连接恢复：新的 WebSocket 完成握手，收到 101，可以收发消息。",{"type":41,"children":289},[290],{"text":291,"type":29},"业务恢复：断线期间产生的消息被补齐或明确丢弃，恢复后不重复、不乱序，业务状态与服务端一致。",{"type":25,"children":293},[294],{"text":295,"type":29},"连接恢复是必要条件，不是充分条件。客户端重连后如果没有带上恢复凭据，服务端可能按游标、时间窗或自身策略补发部分或全部消息，也可能直接拒绝恢复；策略不同，出问题的形态也不同：全量重放会重复，拒绝恢复会缺失。具体按哪种策略，以协议文档和测试结果为准。",{"type":31,"level":32,"children":297},[298],{"text":299,"type":29},"如何分别制造三种可控断线",{"type":25,"children":301},[302],{"text":303,"type":29},"不同断线方式会触发不同的关闭码与恢复路径，必须分开测：",{"type":25,"children":305},[306],{"text":307,"type":29},"1. 客户端主动关闭：调用 `close(1000)`，按应用预定策略验证停止或重新连接及状态处理；不自动重连不必然表示故障。\n2. 网络层中断：在自建测试网络中丢弃连接、发送 RST 或停止客户端进程等，记录故障点和方向。若客户端报告 1006，将其视为本地未收到有效 Close 帧的异常状态，不预设所有中断都会触发该码。\n3. 服务端主动断开：服务端关闭连接或重启，验证重连后的鉴权与恢复流程。",{"type":25,"children":309},[310],{"text":311,"type":29},"在自建测试环境中制造中断，不对正常业务链路注入故障。先做少量试跑检查测试流程，再根据业务 SLA、风险和观测波动确定正式覆盖次数。断线时机可覆盖空闲期、消息传输中和鉴权刚完成时，并记录 TCP RST、丢包、进程退出、心跳超时等实际证据。",{"type":25,"children":313},[314,316,320],{"text":315,"type":29},"网络层中断的定位可参考",{"text":317,"type":29,"marks":318},"代理 IP 故障排查中心",[319],{"href":191,"type":70},{"text":124,"type":29},{"type":31,"level":32,"children":322},[323],{"text":324,"type":29},"重连策略应该观察哪些指标",{"type":37,"format":38,"children":326},[327,331,335,339,343],{"type":41,"children":328},[329],{"text":330,"type":29},"首次重连延迟：从检测到断线到发出重连请求的时间。",{"type":41,"children":332},[333],{"text":334,"type":29},"重连间隔序列：是否指数退避，是否带随机抖动。",{"type":41,"children":336},[337],{"text":338,"type":29},"重连次数上限：达到上限后是放弃还是降级。",{"type":41,"children":340},[341],{"text":342,"type":29},"鉴权是否重做：令牌过期时是否重新获取。",{"type":41,"children":344},[345],{"text":346,"type":29},"是否携带恢复凭据：resume token、最后一条消息的序号或事件 ID。",{"type":25,"children":348},[349],{"text":350,"type":29},"退避没有抖动时，大量客户端会在同一时刻重连，形成二次冲击。验证时观察间隔序列是否递增且带随机量。",{"type":31,"level":32,"children":352},[353],{"text":354,"type":29},"常见的三种会话恢复机制怎么验证",{"type":37,"format":38,"children":356},[357,361,365],{"type":41,"children":358},[359],{"text":360,"type":29},"服务端保留游标：客户端带上最后确认的序号，服务端从下一条开始推送。",{"type":41,"children":362},[363],{"text":364,"type":29},"客户端拉取历史：重连后主动请求断线时间窗内的消息，再切回实时流。",{"type":41,"children":366},[367],{"text":368,"type":29},"无状态重放：服务端按时间窗重放，客户端负责去重。",{"type":25,"children":370},[371],{"text":372,"type":29},"序号的单调范围可能限于某个分区、订阅或过滤条件；先确认序号作用域、客户端确认点、乱序窗口和过滤规则。三种机制按产品契约分别验证：游标是否被接受、历史拉取与实时流是否衔接、重放窗口是否被正确去重。缺少客户端 token 不代表服务端没有已保存的恢复位置。",{"type":31,"level":32,"children":374},[375],{"text":376,"type":29},"如何检查补发消息是否重复或乱序",{"type":25,"children":378},[379],{"text":380,"type":29},"恢复阶段最容易出现\"补多了\"和\"补乱了\"。校验方法：",{"type":37,"format":38,"children":382},[383,387,391,395],{"type":41,"children":384},[385],{"text":386,"type":29},"每条业务消息带唯一 ID 与单调递增序号。",{"type":41,"children":388},[389],{"text":390,"type":29},"客户端记录已处理 ID 的窗口，重复 ID 直接丢弃。",{"type":41,"children":392},[393],{"text":394,"type":29},"收到消息时按已确认的序号作用域检查连续性；出现缺口后，按协议选择缓冲等待、补拉历史或进入降级\u002F重新同步，不规定所有系统都必须立即停止消费。",{"type":41,"children":396},[397],{"text":398,"type":29},"分别记录重复接收、去重后重复产生的业务副作用、最终缺失条数，作为恢复质量指标。",{"type":25,"children":400},[401],{"text":402,"type":29},"序号不能只靠时间戳，同一毫秒内的多条消息会无法排序。服务端生成序号时保证单调，客户端只做校验与去重。",{"type":31,"level":32,"children":404},[405],{"text":406,"type":29},"如何确认重连后业务状态仍然一致",{"type":25,"children":408},[409],{"text":410,"type":29},"连接层和消息层都正常，业务仍可能不一致。核对三项：",{"type":25,"children":412},[413],{"text":414,"type":29},"1. 关键状态字段：账户余额、订单状态、会话归属是否与服务端一致。\n2. 未确认操作：断线前发出但未收到确认的请求，重连后是否重发、是否被服务端去重。\n3. 幂等性：重发的写操作是否只生效一次（去重机制见上一节）。",{"type":25,"children":416},[417],{"text":418,"type":29},"业务核对要读服务端的权威状态，不能只信客户端本地缓存。本地缓存与服务端不一致时，以服务端为准并记录差异。",{"type":31,"level":32,"children":420},[421],{"text":422,"type":29},"如何记录恢复时间并完成验收",{"type":25,"children":424},[425],{"text":426,"type":29},"恢复时间要拆成两个数：断线到新连接可用，断线到业务补齐完成。前者反映重连策略，后者反映恢复机制。只报前者会低估用户实际等待时间。",{"type":37,"format":38,"children":428},[429,433,437],{"type":41,"children":430},[431],{"text":432,"type":29},"断线类型：客户端 close(1000) ｜ 恢复耗时：1.2 s ｜ 丢失条数：0 ｜ 重复条数：0 ｜ 业务状态一致：是 ｜ 证据：序号连续、状态字段一致",{"type":41,"children":434},[435],{"text":436,"type":29},"断线类型：网络中断（1006） ｜ 恢复耗时：3.5 s ｜ 丢失条数：0 ｜ 重复条数：2 ｜ 业务状态一致：是 ｜ 证据：去重窗口命中、补齐完成",{"type":41,"children":438},[439],{"text":440,"type":29},"断线类型：服务端重启 ｜ 恢复耗时：8.0 s ｜ 丢失条数：0 ｜ 重复条数：0 ｜ 业务状态一致：是 ｜ 证据：重新鉴权、游标续传",{"type":25,"children":442},[443],{"text":444,"type":29},"表中数值仅为示例，实际按测试记录填写；网络中断示例中的 1006 仅表示该次客户端本地观察到异常关闭状态，不代表确定的根因。重复接收不必然是缺陷，需进一步核对去重后的业务副作用。",{"type":25,"children":446},[447],{"text":448,"type":29},"验收表包含断线类型、恢复耗时、最终缺失条数、重复接收数、重复业务副作用、状态一致性和证据位置。覆盖次数与阈值按业务 SLA、风险和测量波动事先确定；3 次或 5 次只能作为试跑示例，不能当成通用充分门槛。正常 `close(1000)` 应按产品预定策略验证停止或恢复，不把不自动重连直接判错。任一关键证据缺失，结论应标注“证据不足”或“观察到重连成功”，不能写“恢复完成”。",{"type":25,"children":450},[451,453,456],{"text":452,"type":29},"测试工具的接入参数可对照",{"text":119,"type":29,"marks":454},[455],{"href":122,"type":70},{"text":124,"type":29},{"type":25,"children":458},[459,461,466],{"text":460,"type":29},"WebSocket 关闭状态和 Close 帧语义见 ",{"text":462,"type":29,"marks":463},"RFC 6455",[464],{"href":465,"type":70},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc6455",{"text":124,"type":29},{"type":31,"level":32,"children":468},[469],{"text":227,"type":29},{"type":25,"children":471},[472],{"text":473,"type":29,"marks":474},"Q：重连成功但消息少了一段，问题出在哪？",[475],{"type":234},{"type":25,"children":477},[478],{"text":479,"type":29},"A：先对照客户端、服务端和网络事件确定可观测到的故障位置，再按协议检查恢复位置。恢复状态可能来自客户端凭据，也可能由服务端保存；没有客户端序号不能单独证明服务端无法续传。",{"type":25,"children":481},[482],{"text":483,"type":29,"marks":484},"Q：close code 1006 一定能复现吗？",[485],{"type":234},{"type":25,"children":487},[488],{"text":489,"type":29},"A：不保证。1006 是本地报告的异常关闭状态，表示没有收到有效 Close 帧；不同客户端实现和故障方式下的观测结果可能不同，需记录具体故障注入和客户端事件。",{"type":25,"children":491},[492],{"text":493,"type":29,"marks":494},"Q：重复消息一定要在客户端去重吗？",[495],{"type":234},{"type":25,"children":497},[498],{"text":499,"type":29},"A：取决于协议契约。服务端游标续传、时间窗重放和客户端去重的责任不同，应分别检查收到重复与重复业务副作用，不能只统计客户端收到的消息数。",{"source":6,"id":501,"documentId":502,"translationKey":502,"locale":9,"slug":503,"path":504,"_path":504,"title":505,"description":506,"excerpt":506,"image":507,"publishedAt":508,"updatedAt":509,"author":17,"tags":510,"featured":19,"sortOrder":20,"readingTime":272,"bodyType":22,"bodyBlocks":511,"bodyHtml":259},3936,"wiumgl5vcc6qclqx4xe26751","websocket-proxy-test-report-metrics","\u002Fblog\u002Fwebsocket-proxy-test-report-metrics","WebSocket 代理IP 测试报告应该记录哪些指标","一份只能写「连接成功、延迟正常」的 WebSocket 测试报告，问题不在结论而在字段。把报告拆成环境、连接、业务三层，逐项给出字段、单位、采集方式与用途，配合握手分段、断连码分布、重连次数与 RTT 分位数，形成可直接落表、可复核的模板。","https:\u002F\u002Fcms.quanqiudaili.com\u002Fuploads\u002Flarge_Web_Socket_cover_bc45bc1e13.webp","2026-11-04T06:50:00.175Z","2026-11-04T06:50:00.160Z",[],[512,516,520,534,538,542,546,564,568,581,585,607,611,615,637,641,645,649,683,687,691,695,713,717,721,725,729,738,741,747,751,757,761,767],{"type":25,"children":513},[514],{"text":515,"type":29},"一份只能写\"连接成功、延迟正常\"的 WebSocket 报告，问题在于缺少可复核字段。本文按环境、连接、业务三层列出报告项，并注明采集工具与可观测边界。浏览器标准 WebSocket 事件通常不能直接提供全部握手分段、原始响应头或每一跳耗时；拿不到的字段应标为 N\u002FA 并说明原因。",{"type":31,"level":32,"children":517},[518],{"text":519,"type":29},"WebSocket 代理测试报告应该分哪三层记录",{"type":37,"format":38,"children":521},[522,526,530],{"type":41,"children":523},[524],{"text":525,"type":29},"环境层：这次测试在什么条件下发生。地区、出口 IP 与 ASN、代理协议、客户端版本、测试时间窗。",{"type":41,"children":527},[528],{"text":529,"type":29},"连接层：连接本身发生了什么。握手各段耗时、会话寿命、关闭码、异常关闭原因。",{"type":41,"children":531},[532],{"text":533,"type":29},"业务层：连接之上传了什么。消息计数、往返时延、丢失与重复、顺序连续性、恢复耗时。",{"type":25,"children":535},[536],{"text":537,"type":29},"三层分开写的好处是定位快：业务层异常先看连接层是否断过，连接层异常再看环境层是否出口漂移。混在一起写，读者只能看到\"整体不好\"。",{"type":31,"level":32,"children":539},[540],{"text":541,"type":29},"握手和建连阶段要记录哪些字段",{"type":25,"children":543},[544],{"text":545,"type":29},"握手要分四段记录，不能只写总和：",{"type":37,"format":38,"children":547},[548,552,556,560],{"type":41,"children":549},[550],{"text":551,"type":29},"DNS 解析耗时（ms）。",{"type":41,"children":553},[554],{"text":555,"type":29},"TCP 建连耗时（ms）。",{"type":41,"children":557},[558],{"text":559,"type":29},"TLS 握手耗时（ms，wss 才有）。",{"type":41,"children":561},[562],{"text":563,"type":29},"Upgrade 往返耗时（ms）：请求发出到收到 `101 Switching Protocols`。",{"type":25,"children":565},[566],{"text":567,"type":29},"同时记录校验结果：`Sec-WebSocket-Accept` 是否与服务端计算值一致、协商的子协议与扩展是否与预期相同、响应头是否被中间设备改写。握手失败要记录失败阶段与错误码，而不是只记\"连接失败\"。",{"type":25,"children":569},[570,572,575,577,580],{"text":571,"type":29},"握手前的代理参数可对照",{"text":119,"type":29,"marks":573},[574],{"href":122,"type":70},{"text":576,"type":29},"；Upgrade 握手字段定义见 ",{"text":462,"type":29,"marks":578},[579],{"href":465,"type":70},{"text":124,"type":29},{"type":31,"level":32,"children":582},[583],{"text":584,"type":29},"连接持续多久、为什么断开要怎么记录",{"type":37,"format":38,"children":586},[587,591,595,599,603],{"type":41,"children":588},[589],{"text":590,"type":29},"会话观察时长（s）：从收到 101 到关闭或观察窗口结束；窗口结束仍在线单独计数，不当作自然断开。",{"type":41,"children":592},[593],{"text":594,"type":29},"关闭发起方：只有两端事件或网络日志能佐证时才填写；无法确认则记未知。",{"type":41,"children":596},[597],{"text":598,"type":29},"本地关闭事件码与实际 Close 帧分开记录。1006 是本地报告的异常关闭状态，不是线上发送的 Close 帧；正常码也不能单独证明业务正常。",{"type":41,"children":600},[601],{"text":602,"type":29},"异常关闭率：明确分子、分母和观察窗口；握手失败、已关闭连接与窗口结束仍在线的连接分别统计。",{"type":41,"children":604},[605],{"text":606,"type":29},"心跳配置：间隔与超时阈值（s），以及心跳是否被中间设备吞掉。",{"type":25,"children":608},[609],{"text":610,"type":29},"关闭事件码分布可帮助筛选样本，但不能仅凭 code 判定发起方或根因。1006 表示本地未观察到有效 Close 帧；需结合客户端、服务端或网络侧日志继续定位。",{"type":31,"level":32,"children":612},[613],{"text":614,"type":29},"重连过程应该记录哪些指标",{"type":37,"format":38,"children":616},[617,621,625,629,633],{"type":41,"children":618},[619],{"text":620,"type":29},"首次重连延迟（ms\u002Fs）：从检测到断线到发起重连。",{"type":41,"children":622},[623],{"text":624,"type":29},"重连次数与退避序列：是否按指数退避，是否有抖动。",{"type":41,"children":626},[627],{"text":628,"type":29},"重连成功率：成功建立新连接的比例。",{"type":41,"children":630},[631],{"text":632,"type":29},"恢复耗时：从断线到业务可用的时间。",{"type":41,"children":634},[635],{"text":636,"type":29},"是否携带恢复凭据：如 resume token、last sequence。",{"type":25,"children":638},[639],{"text":640,"type":29},"重连次数多不代表质量差，可能是服务端主动轮转；但没有退避、密集重连会把故障放大。报告应同时给次数与间隔分布。",{"type":31,"level":32,"children":642},[643],{"text":644,"type":29},"如何汇总三层字段并补充消息细节",{"type":25,"children":646},[647],{"text":648,"type":29},"下表把三层字段汇总成可直接落表的形式。",{"type":37,"format":38,"children":650},[651,655,659,663,667,671,675,679],{"type":41,"children":652},[653],{"text":654,"type":29},"字段组：握手 ｜ 字段：DNS \u002F TCP \u002F TLS \u002F Upgrade ｜ 单位：ms ｜ 采集方式：受控网络日志、协议工具或服务端\u002F代理日志；浏览器标准事件通常不可提供全部分段 ｜ 用途：定位可观测范围内的建连耗时",{"type":41,"children":656},[657],{"text":658,"type":29},"字段组：连接 ｜ 字段：观察时长、结束状态、仍在线数 ｜ 单位：s \u002F 次 ｜ 采集方式：连接事件与观察窗口 ｜ 用途：区分自然断开与测试截止",{"type":41,"children":660},[661],{"text":662,"type":29},"字段组：连接 ｜ 字段：本地关闭事件码、Close 帧收发、可证实的发起方 ｜ 单位：次数 ｜ 采集方式：客户端事件及可用两端日志；不可观测时记 N\u002FA ｜ 用途：作为定位线索，不单独归因",{"type":41,"children":664},[665],{"text":666,"type":29},"字段组：重连 ｜ 字段：重连次数与间隔 ｜ 单位：次 \u002F s ｜ 采集方式：重连逻辑埋点 ｜ 用途：评估退避是否合理",{"type":41,"children":668},[669],{"text":670,"type":29},"字段组：消息 ｜ 字段：收发计数 ｜ 单位：条 ｜ 采集方式：消息回调 ｜ 用途：核对是否丢消息",{"type":41,"children":672},[673],{"text":674,"type":29},"字段组：消息 ｜ 字段：RTT p50 \u002F p95 ｜ 单位：ms ｜ 采集方式：发送与回执时间戳 ｜ 用途：评估体感延迟",{"type":41,"children":676},[677],{"text":678,"type":29},"字段组：消息 ｜ 字段：序号缺口与重复数 ｜ 单位：条 ｜ 采集方式：序号连续性校验 ｜ 用途：判断顺序与去重",{"type":41,"children":680},[681],{"text":682,"type":29},"字段组：代理IP ｜ 字段：类型、出口和会话分配 ｜ 单位：枚举 \u002F 地址 \u002F ID ｜ 采集方式：供应商控制台、分配记录及受控回显 ｜ 用途：说明本次测试路径，不能单独证明物理路由",{"type":25,"children":684},[685],{"text":686,"type":29},"表外还要补两项：消息大小分布与发送频率，否则 RTT 无法横向比较。大消息和小消息的往返时间差异明显，混在一起算分位数会失真。",{"type":31,"level":32,"children":688},[689],{"text":690,"type":29},"地区和代理链路还要记录哪些信息",{"type":25,"children":692},[693],{"text":694,"type":29},"代理IP字段记录实际分配的产品类型、出口和会话变化；无法确认的属性应标为未知。",{"type":37,"format":38,"children":696},[697,701,705,709],{"type":41,"children":698},[699],{"text":700,"type":29},"地区：客户端所在地区，与出口地区分开写。",{"type":41,"children":702},[703],{"text":704,"type":29},"出口 IP 与 ASN：每条连接记录一次，用于验证地区标签。",{"type":41,"children":706},[707],{"text":708,"type":29},"代理协议与会话类型：HTTP、SOCKS5，短会话或长会话；品类按实际分配写动态或静态住宅 IP、动态或静态机房 IP，不写笼统的\"住宅或机房\"，并保留出口 IP 与分配时间。",{"type":41,"children":710},[711],{"text":712,"type":29},"链路跳数：若代理有多跳，记录跳数与各跳耗时。",{"type":25,"children":714},[715],{"text":716,"type":29},"出口 IP 是测试记录的重要字段，但需与服务端回显、代理分配记录和 DNS\u002F调度信息结合；它本身不能证明物理路径或目标服务调度。",{"type":31,"level":32,"children":718},[719],{"text":720,"type":29},"报告模板应该怎样组织和统计",{"type":25,"children":722},[723],{"text":724,"type":29},"模板按\"环境 → 连接 → 业务\"顺序排列，每部分先给汇总表，再列异常样本。统计时报告 p50、p95 和样本数，单次最快值不能代表整体表现。\n失败与超时单独计数，不混入成功样本的分位数。\n按配置地区、出口和时间窗分组；跨窗口比较时同时标明环境差异。",{"type":25,"children":726},[727],{"text":728,"type":29},"异常样本保留 request id、连接 ID、出口 IP、关闭事件、时间戳及可用证据。报告结论写明客户端版本、服务端环境、测试时长、采集工具和不可观测字段；详细握手数据需受控网络日志、协议工具或服务端\u002F代理日志支持。",{"type":25,"children":730},[731,733,736],{"text":732,"type":29},"连接异常样本可按",{"text":317,"type":29,"marks":734},[735],{"href":191,"type":70},{"text":737,"type":29},"的字段顺序补充。",{"type":31,"level":32,"children":739},[740],{"text":227,"type":29},{"type":25,"children":742},[743],{"text":744,"type":29,"marks":745},"Q：报告里必须记录出口 IP 吗？",[746],{"type":234},{"type":25,"children":748},[749],{"text":750,"type":29},"A：建议记录，并同时记代理IP类型。它是验证地区标签与判断出口漂移的重要直接证据之一，但要与服务端回显、代理记录一起看，缺失后地区结论只能靠推断。",{"type":25,"children":752},[753],{"text":754,"type":29,"marks":755},"Q：close code 只记 1006 够不够？",[756],{"type":234},{"type":25,"children":758},[759],{"text":760,"type":29},"A：不够。关闭码是协议层线索，不能单独判断发起方或根因。应同时记录本地事件、实际 Close 帧收发和可取得的两端日志，并明确异常关闭率的分子、分母和仍在线连接的处理方式。",{"type":25,"children":762},[763],{"text":764,"type":29,"marks":765},"Q：时延只报平均值可以吗？",[766],{"type":234},{"type":25,"children":768},[769],{"text":770,"type":29},"A：不建议。平均值会掩盖尾部延迟，应同时报 p50 与 p95，并注明样本量。",{"source":6,"id":772,"documentId":773,"translationKey":773,"locale":9,"slug":774,"path":775,"_path":775,"title":776,"description":777,"excerpt":777,"image":778,"publishedAt":779,"updatedAt":780,"author":17,"tags":781,"featured":19,"sortOrder":20,"readingTime":21,"bodyType":22,"bodyBlocks":782,"bodyHtml":259},3935,"apg9xmkn8f0bozwgyxntuhtj","python-test-proxy-environment-variable-isolation","\u002Fblog\u002Fpython-test-proxy-environment-variable-isolation","Python 自动化测试：代理IP 环境变量污染如何验证","同一份测试在开发机能过、到 CI 就失败，常常是残留的代理变量在捣乱。文章列出这些变量及其读取顺序，用 fixture 固定环境、请求前断言、回显出口 IP 证明链路，再给出清理检查与故意失败自检。","https:\u002F\u002Fcms.quanqiudaili.com\u002Fuploads\u002Flarge_Python_cover_58829f8376.webp","2026-11-04T06:49:42.792Z","2026-11-04T06:49:42.779Z",[],[783,787,791,801,805,809,813,817,835,839,843,847,851,855,863,867,871,875,879,883,893,897,901,927,931,935,939,953,957,961,965,979,983,998,1002,1006,1010,1013,1019,1023,1029,1033,1039],{"type":25,"children":784},[785],{"text":786,"type":29},"同一份测试，在开发机上通过、在 CI 上失败，或者反过来——十有八九是环境里的代理变量在起作用。requests、urllib、httpx 等常见实现通常会读取环境变量，具体行为随库和版本而变；CI 镜像又常预置 `HTTP_PROXY`。判断\"有没有被污染\"不能靠肉眼扫代码，要靠在测试内部固定环境并断言出口。这些变量指向的代理IP，往往和用例约定的不是同一条。NO_PROXY 规则本身的行为验证属于另一个话题，本篇讲的是测试框架里的环境隔离。",{"type":31,"level":32,"children":788},[789],{"text":790,"type":29},"代理环境变量污染通常会表现为什么",{"type":37,"format":38,"children":792},[793,797],{"type":41,"children":794},[795],{"text":796,"type":29},"意外走代理：本应直连的请求，被环境变量送进了代理，出口 IP 与预期不符。",{"type":41,"children":798},[799],{"text":800,"type":29},"意外绕过代理：本应走代理的请求，被 `NO_PROXY` 或空值放行，直接连到了目标。",{"type":25,"children":802},[803],{"text":804,"type":29},"两种表现都会让断言\"通过得莫名其妙\"。前者常出现在本地缓存或 mock 场景，后者常出现在 CI 里变量拼写错误或作用域不对的场景。判定污染不看代码写了什么，看实际请求从哪里出去。",{"type":31,"level":32,"children":806},[807],{"text":808,"type":29},"哪些代理环境变量会生效，优先级如何判断",{"type":25,"children":810},[811],{"text":812,"type":29},"变量不是设了就生效，读取顺序、大小写和库的默认值都会改变结果；变量只描述地址和协议，不描述代理IP的身份，身份得另做出口校验。把\"默认\"当成待实测的假设：按库和版本分别验证，并补一个故意设错变量的负向用例。",{"type":31,"level":272,"children":814},[815],{"text":816,"type":29},"为什么要同时检查大小写变量和 ALL_PROXY",{"type":37,"format":38,"children":818},[819,823,827,831],{"type":41,"children":820},[821],{"text":822,"type":29},"变量：`HTTP_PROXY` \u002F `http_proxy` ｜ 作用范围：http 请求 ｜ Python 客户端是否读取：requests、urllib 默认读取 ｜ 测试中如何处理：fixture 内显式设置或清空",{"type":41,"children":824},[825],{"text":826,"type":29},"变量：`HTTPS_PROXY` \u002F `https_proxy` ｜ 作用范围：https 请求 ｜ Python 客户端是否读取：同上 ｜ 测试中如何处理：同上",{"type":41,"children":828},[829],{"text":830,"type":29},"变量：`ALL_PROXY` \u002F `all_proxy` ｜ 作用范围：未单独指定协议时 ｜ Python 客户端是否读取：requests 读取 ｜ 测试中如何处理：容易遗漏，需一并清理",{"type":41,"children":832},[833],{"text":834,"type":29},"变量：`NO_PROXY` \u002F `no_proxy` ｜ 作用范围：排除列表 ｜ Python 客户端是否读取：requests、urllib 读取 ｜ 测试中如何处理：边界用例单独覆盖",{"type":25,"children":836},[837],{"text":838,"type":29},"`ALL_PROXY` 最容易被漏：未单独指定协议时它接管全部请求，清理时不能只清 HTTP_PROXY 与 HTTPS_PROXY。",{"type":31,"level":272,"children":840},[841],{"text":842,"type":29},"环境变量大小写相同时应该相信哪一个",{"type":25,"children":844},[845],{"text":846,"type":29},"同名不同大小写时，取值顺序可能相反。requests 通过 `urllib.request.getproxies()` 取值，常见实现小写优先，但版本和平台不同结果会变，也有库只认大写。测试里不要依赖，统一由 fixture 显式设定，并在请求前断言生效值，再用实际出口校验。",{"type":31,"level":272,"children":848},[849],{"text":850,"type":29},"为什么没写代理的 requests 仍可能走代理",{"type":25,"children":852},[853],{"text":854,"type":29},"Python 的 `requests` 常见版本默认 `trust_env=True`，会读取环境配置；该开关还会影响 `.netrc` 等其他来源。需要隔离环境代理时，优先为测试创建专用 `Session` 并设置 `trust_env=False`，再显式配置测试所需的认证与 CA。这样也会关闭该 Session 的其他环境来源，需按实际需求补配。不要把只对 `http`、`https` 设置 `None` 当成覆盖所有代理环境变量的通用方法；应按 requests 版本、目标地址和实际出口验证。",{"type":25,"children":856},[857,859,862],{"text":858,"type":29},"代理环境变量与请求库的关系可对照",{"text":119,"type":29,"marks":860},[861],{"href":122,"type":70},{"text":124,"type":29},{"type":31,"level":32,"children":864},[865],{"text":866,"type":29},"如何用 fixture 固定环境并提前断言",{"type":25,"children":868},[869],{"text":870,"type":29},"在测试层做三件事：",{"type":25,"children":872},[873],{"text":874,"type":29},"1. autouse fixture 在用例开始前清空所有代理变量（大小写各一份），用例结束后恢复。\n2. 需要代理的用例通过参数显式注入，不依赖外部环境。\n3. 请求发出前断言：`os.environ` 中的值、`session.proxies`、`session.trust_env` 与预期一致。",{"type":25,"children":876},[877],{"text":878,"type":29},"```python\nimport pytest",{"type":25,"children":880},[881],{"text":882,"type":29},"@pytest.fixture(autouse=True)\ndef clean_proxy_env(monkeypatch):\nfor key in (\"HTTP_PROXY\", \"HTTPS_PROXY\", \"ALL_PROXY\",\n\"NO_PROXY\", \"http_proxy\", \"https_proxy\", \"all_proxy\",\n\"no_proxy\"):\nmonkeypatch.delenv(key, raising=False)\n```",{"type":25,"children":884},[885,887,892],{"text":886,"type":29},"需要测试 `NO_PROXY` 时，在对应参数化用例中单独设置目标值。`monkeypatch` 会在用例结束时恢复原环境；可加一个检查，确认所有大小写变量恢复到用例前状态。详情见 ",{"text":888,"type":29,"marks":889},"pytest monkeypatch 文档",[890],{"href":891,"type":70},"https:\u002F\u002Fdocs.pytest.org\u002Fen\u002Fstable\u002Fhow-to\u002Fmonkeypatch.html",{"text":124,"type":29},{"type":31,"level":32,"children":894},[895],{"text":896,"type":29},"NO_PROXY 哪些边界情况必须单独测试",{"type":25,"children":898},[899],{"text":900,"type":29},"`NO_PROXY` 的匹配规则容易想当然，至少要覆盖：",{"type":37,"format":38,"children":902},[903,907,911,915,919,923],{"type":41,"children":904},[905],{"text":906,"type":29},"精确域名与子域名：`example.com` 是否覆盖 `api.example.com`。",{"type":41,"children":908},[909],{"text":910,"type":29},"端口写法：`example.com:443` 与不带端口的差异。",{"type":41,"children":912},[913],{"text":914,"type":29},"IP 与 CIDR：单 IP、网段写法是否被支持。",{"type":41,"children":916},[917],{"text":918,"type":29},"本机地址：`localhost`、`127.0.0.1`、`::1`。",{"type":41,"children":920},[921],{"text":922,"type":29},"通配：`*` 是否让所有请求直连。",{"type":41,"children":924},[925],{"text":926,"type":29},"空值：变量存在但为空字符串时的行为。",{"type":25,"children":928},[929],{"text":930,"type":29},"每个边界写成一个用例，断言\"是否走了代理\"，而不是\"变量是否被读取\"。变量被读取但匹配失败，同样属于污染。",{"type":31,"level":32,"children":932},[933],{"text":934,"type":29},"如何证明请求真的走了预期出口",{"type":25,"children":936},[937],{"text":938,"type":29},"环境变量只是配置，出口才是结果。回显请求必须由同一个被测客户端、协议和目标匹配规则发出；另用受控目标的访问日志或代理到达记录核对真正的业务请求。不同目标可能命中不同的 `NO_PROXY` 规则，单独请求另一个回显域名不能证明被测请求走了同一路径。直连出口应现场测量，并注明 NAT 或 CI 共享出口的影响：",{"type":37,"format":38,"children":940},[941,945,949],{"type":41,"children":942},[943],{"text":944,"type":29},"期望走代理：出口应属于代理侧地址段。",{"type":41,"children":946},[947],{"text":948,"type":29},"期望直连：出口应属于本机公网地址段。",{"type":41,"children":950},[951],{"text":952,"type":29},"期望绕过：目标应命中 `NO_PROXY`，出口为直连地址。",{"type":25,"children":954},[955],{"text":956,"type":29},"回显服务用自建或受控端点。分别覆盖预期走代理、命中绕过规则、故意设置错误配置和恢复后的请求，并记录目标、时间、客户端版本与代理到达证据。代理 URL 只记录脱敏后的协议、主机、端口和配置来源；删除 userinfo、令牌及敏感查询参数，避免 CI 日志泄露凭据。",{"type":31,"level":32,"children":958},[959],{"text":960,"type":29},"如何隔离开发机和 CI 继承的代理环境",{"type":25,"children":962},[963],{"text":964,"type":29},"本地 shell 里的代理变量会通过 `pytest` 进程继承进来，CI 的容器镜像也可能预置。隔离手段：",{"type":37,"format":38,"children":966},[967,971,975],{"type":41,"children":968},[969],{"text":970,"type":29},"在 `pytest.ini` 或 `conftest.py` 里统一清理，不依赖开发者本地配置。",{"type":41,"children":972},[973],{"text":974,"type":29},"CI 任务显式声明环境，不继承 runner 的全局变量。",{"type":41,"children":976},[977],{"text":978,"type":29},"需要真实代理的集成测试单独标记，默认不跑。",{"type":25,"children":980},[981],{"text":982,"type":29},"区分\"单元测试\"与\"集成测试\"的边界很重要：前者不应依赖任何外部出口，后者才需要真实代理。",{"type":25,"children":984},[985,987,992,994,997],{"text":986,"type":29},"requests 的环境设置行为见 ",{"text":988,"type":29,"marks":989},"Requests 高级用法文档",[990],{"href":991,"type":70},"https:\u002F\u002Frequests.readthedocs.io\u002Fen\u002Fstable\u002Fuser\u002Fadvanced\u002F",{"text":993,"type":29},"；测试环境变量恢复可参考 ",{"text":888,"type":29,"marks":995},[996],{"href":891,"type":70},{"text":124,"type":29},{"type":31,"level":32,"children":999},[1000],{"text":1001,"type":29},"如何通过清理检查和故意失败验证测试有效",{"type":25,"children":1003},[1004],{"text":1005,"type":29},"最后一步是验证测试本身有效：把预期出口故意写错，测试必须失败。如果仍然通过，说明断言没有真正落到出口上，前面所有隔离都只是形式。",{"type":25,"children":1007},[1008],{"text":1009,"type":29},"清理检查同样要做：用例结束后确认 `os.environ` 已恢复，没有把变量泄漏给后续用例。多个用例串行时，泄漏会表现为\"只有第一个用例正常\"。",{"type":31,"level":32,"children":1011},[1012],{"text":227,"type":29},{"type":25,"children":1014},[1015],{"text":1016,"type":29,"marks":1017},"Q：代码里没写代理，还会被环境变量影响吗？",[1018],{"type":234},{"type":25,"children":1020},[1021],{"text":1022,"type":29},"A：会。requests 等常见实现默认读取环境变量，`trust_env` 为 True 时自动生效；具体行为以实际版本为准，测试里要显式清理或关闭，并回显出口验证。",{"type":25,"children":1024},[1025],{"text":1026,"type":29,"marks":1027},"Q：`NO_PROXY` 写了域名，为什么子域名还是走了代理？",[1028],{"type":234},{"type":25,"children":1030},[1031],{"text":1032,"type":29},"A：不同实现对子域名的匹配规则不同，需要按库的实际行为写用例验证，不能假设后缀匹配一定生效。",{"type":25,"children":1034},[1035],{"text":1036,"type":29,"marks":1037},"Q：CI 上通过、本地失败，先查什么？",[1038],{"type":234},{"type":25,"children":1040},[1041],{"text":1042,"type":29},"A：先比对变量是否存在、脱敏后的协议\u002F主机\u002F端口和配置来源，再用同一被测请求的目标日志或代理到达记录确认路径。不要打印完整代理 URL，其中可能含账号、密码或令牌。",[1044],{"source":6,"id":7,"documentId":8,"translationKey":8,"locale":9,"slug":10,"path":11,"_path":11,"title":12,"description":13,"excerpt":13,"image":14,"publishedAt":15,"updatedAt":16,"author":17,"tags":1045,"featured":19,"sortOrder":20,"readingTime":21,"bodyType":22,"bodyBlocks":1046,"bodyHtml":259},[],[1047,1050,1053,1061,1064,1067,1070,1077,1080,1083,1088,1102,1105,1116,1119,1122,1125,1140,1143,1146,1157,1160,1167,1170,1184,1187,1190,1193,1198,1201,1206,1209,1214],{"type":25,"children":1048},[1049],{"text":28,"type":29},{"type":31,"level":32,"children":1051},[1052],{"text":35,"type":29},{"type":37,"format":38,"children":1054},[1055,1058],{"type":41,"children":1056},[1057],{"text":44,"type":29},{"type":41,"children":1059},[1060],{"text":48,"type":29},{"type":25,"children":1062},[1063],{"text":52,"type":29},{"type":31,"level":32,"children":1065},[1066],{"text":56,"type":29},{"type":25,"children":1068},[1069],{"text":60,"type":29},{"type":25,"children":1071},[1072,1073,1076],{"text":64,"type":29},{"text":66,"type":29,"marks":1074},[1075],{"href":69,"type":70},{"text":72,"type":29},{"type":31,"level":32,"children":1078},[1079],{"text":76,"type":29},{"type":25,"children":1081},[1082],{"text":80,"type":29},{"type":37,"format":38,"children":1084},[1085],{"type":41,"children":1086},[1087],{"text":86,"type":29},{"type":37,"format":38,"children":1089},[1090,1093,1096,1099],{"type":41,"children":1091},[1092],{"text":92,"type":29},{"type":41,"children":1094},[1095],{"text":96,"type":29},{"type":41,"children":1097},[1098],{"text":100,"type":29},{"type":41,"children":1100},[1101],{"text":104,"type":29},{"type":25,"children":1103},[1104],{"text":108,"type":29},{"type":25,"children":1106},[1107,1108,1111,1112,1115],{"text":112,"type":29},{"text":66,"type":29,"marks":1109},[1110],{"href":69,"type":70},{"text":117,"type":29},{"text":119,"type":29,"marks":1113},[1114],{"href":122,"type":70},{"text":124,"type":29},{"type":31,"level":32,"children":1117},[1118],{"text":128,"type":29},{"type":25,"children":1120},[1121],{"text":132,"type":29},{"type":25,"children":1123},[1124],{"text":136,"type":29},{"type":37,"format":38,"children":1126},[1127,1130,1137],{"type":41,"children":1128},[1129],{"text":142,"type":29},{"type":41,"children":1131},[1132,1133,1136],{"text":146,"type":29},{"text":148,"type":29,"marks":1134},[1135],{"href":151,"type":70},{"text":124,"type":29},{"type":41,"children":1138},[1139],{"text":156,"type":29},{"type":31,"level":32,"children":1141},[1142],{"text":160,"type":29},{"type":25,"children":1144},[1145],{"text":164,"type":29},{"type":37,"format":38,"children":1147},[1148,1151,1154],{"type":41,"children":1149},[1150],{"text":170,"type":29},{"type":41,"children":1152},[1153],{"text":174,"type":29},{"type":41,"children":1155},[1156],{"text":178,"type":29},{"type":25,"children":1158},[1159],{"text":182,"type":29},{"type":25,"children":1161},[1162,1163,1166],{"text":186,"type":29},{"text":188,"type":29,"marks":1164},[1165],{"href":191,"type":70},{"text":193,"type":29},{"type":31,"level":32,"children":1168},[1169],{"text":197,"type":29},{"type":37,"format":38,"children":1171},[1172,1175,1178,1181],{"type":41,"children":1173},[1174],{"text":203,"type":29},{"type":41,"children":1176},[1177],{"text":207,"type":29},{"type":41,"children":1179},[1180],{"text":211,"type":29},{"type":41,"children":1182},[1183],{"text":215,"type":29},{"type":25,"children":1185},[1186],{"text":219,"type":29},{"type":25,"children":1188},[1189],{"text":223,"type":29},{"type":31,"level":32,"children":1191},[1192],{"text":227,"type":29},{"type":25,"children":1194},[1195],{"text":231,"type":29,"marks":1196},[1197],{"type":234},{"type":25,"children":1199},[1200],{"text":238,"type":29},{"type":25,"children":1202},[1203],{"text":242,"type":29,"marks":1204},[1205],{"type":234},{"type":25,"children":1207},[1208],{"text":248,"type":29},{"type":25,"children":1210},[1211],{"text":252,"type":29,"marks":1212},[1213],{"type":234},{"type":25,"children":1215},[1216],{"text":258,"type":29},null,[],[]]