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