[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-strapi-article:zh:ai-sdk-upgrade-proxy-regression-testing":3,"blog-article:\u002Fblog\u002Fai-sdk-upgrade-proxy-regression-testing":972,"blog-related:\u002Fblog\u002Fai-sdk-upgrade-proxy-regression-testing":973,"blog-translations:\u002Fblog\u002Fai-sdk-upgrade-proxy-regression-testing":974},{"available":4,"article":5,"relatedArticles":228,"translations":822},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":227},"strapi",3891,"t3y9y0xtyubxkwizt5rp9ruu","zh","ai-sdk-upgrade-proxy-regression-testing","\u002Fblog\u002Fai-sdk-upgrade-proxy-regression-testing","AI SDK 升级后，哪些代理连接和请求行为需要重新验证","SDK、HTTP 客户端或代理 agent 升级后，代理 IP 的认证、复用、TLS、重试和流式行为都可能悄悄变化，按七项清单逐项回归并记录版本与证据。","https:\u002F\u002Fcms.quanqiudaili.com\u002Fuploads\u002Flarge_AI_SDK_cover_39e81e86cc.webp","2026-11-03T02:57:23.416Z","2026-11-03T02:57:23.401Z","Global Proxies Team",[],false,0,3,"blocks",[24,30,36,40,57,61,65,69,99,103,107,111,115,119,123,137,141,145,149,153,157,161,165,169,173,192,196,203,207,213,217,223],{"type":25,"children":26},"paragraph",[27],{"text":28,"type":29},"SDK 升级的回归范围，取决于这次实际换了哪一层。可能只是 SDK 版本变化，也可能连底层 HTTP 客户端和代理 agent 一起换了。代理 IP 的接入方式也可能被顺手改掉。先确认改动层级，再按固定清单逐项验证，避免只测一次“能请求成功”就算完成。测试使用测试账号和测试额度，具体行为以目标 SDK 的发布说明为准。","text",{"type":31,"level":32,"children":33},"heading",2,[34],{"text":35,"type":29},"升级了运行时、SDK 还是 HTTP 客户端？",{"type":25,"children":37},[38],{"text":39,"type":29},"先看依赖变更清单，确认本次升级触及哪些层：",{"type":41,"format":42,"children":43},"list","unordered",[44,49,53],{"type":45,"children":46},"list-item",[47],{"text":48,"type":29},"层级：SDK 自身 ｜ 典型变化：版本号、方法签名、默认参数 ｜ 影响：请求构造与重试策略",{"type":45,"children":50},[51],{"text":52,"type":29},"层级：HTTP 客户端 ｜ 典型变化：undici、axios、net\u002Fhttp 或底层库版本 ｜ 影响：连接复用、代理支持、超时语义",{"type":45,"children":54},[55],{"text":56,"type":29},"层级：代理 agent ｜ 典型变化：http-proxy-agent、HTTPS_PROXY 处理 ｜ 影响：代理参数是否被真正应用",{"type":25,"children":58},[59],{"text":60,"type":29},"只升了 SDK，但底层客户端被提升了大版本，回归范围应按底层算。锁文件里同时变化时，尤其要看清。",{"type":31,"level":32,"children":62},[63],{"text":64,"type":29},"SDK 升级后，哪些代理行为需要重新测试？",{"type":25,"children":66},[67],{"text":68,"type":29},"用下表确定回归范围，后续各节按项目展开：",{"type":41,"format":42,"children":70},[71,75,79,83,87,91,95],{"type":45,"children":72},[73],{"text":74,"type":29},"行为：认证头透传 ｜ 怎么观察：抓包看目标域，代理与上游是否各带各的头 ｜ 展开位置：认证透传与重定向",{"type":45,"children":76},[77],{"text":78,"type":29},"行为：跨域重定向 ｜ 怎么观察：重定向到其他域名后是否仍带认证头 ｜ 展开位置：认证透传与重定向",{"type":45,"children":80},[81],{"text":82,"type":29},"行为：连接复用 ｜ 怎么观察：连续请求是否复用同一连接 ｜ 展开位置：连接复用、TLS 与超时重试",{"type":45,"children":84},[85],{"text":86,"type":29},"行为：超时语义 ｜ 怎么观察：建连、首字节、整体三类超时是否各按预期触发 ｜ 展开位置：连接复用、TLS 与超时重试",{"type":45,"children":88},[89],{"text":90,"type":29},"行为：重试行为 ｜ 怎么观察：失败后实际发出的请求次数与退避间隔 ｜ 展开位置：连接复用、TLS 与超时重试",{"type":45,"children":92},[93],{"text":94,"type":29},"行为：流式响应 ｜ 怎么观察：是否逐块到达、代理是否缓冲 ｜ 展开位置：流式响应与 SSE",{"type":45,"children":96},[97],{"text":98,"type":29},"行为：代理参数优先级 ｜ 怎么观察：显式参数、环境变量、默认值谁生效 ｜ 展开位置：代理参数优先级与环境变量",{"type":25,"children":100},[101],{"text":102,"type":29},"清单固定的好处是：下次升级可以逐项打勾，不用重新想测什么。",{"type":31,"level":32,"children":104},[105],{"text":106,"type":29},"升级后认证信息和重定向是否仍按预期工作？",{"type":25,"children":108},[109],{"text":110,"type":29},"代理认证（`Proxy-Authorization`）发给代理，上游认证（`Authorization`）发给授权 API；跨域重定向时应确认上游凭据没有带到新域名。",{"type":25,"children":112},[113],{"text":114,"type":29},"出现 401 时应核对上游鉴权，而不能因代理可达就排除密钥失效。检查请求目标、脱敏后认证头是否存在、密钥状态、scope\u002F项目配置及服务方错误体。只记录头名称、发送目标域和“是否存在”的脱敏状态，不记录 `Authorization` 与 `Proxy-Authorization` 的取值。",{"type":31,"level":32,"children":116},[117],{"text":118,"type":29},"怎么检查连接复用、TLS 和超时重试变化？",{"type":25,"children":120},[121],{"text":122,"type":29},"这三项决定“看起来成功、实际重复请求”的问题，也是升级后语义最容易变的三个地方。",{"type":41,"format":42,"children":124},[125,129,133],{"type":45,"children":126},[127],{"text":128,"type":29},"连接复用：优先看客户端连接池的连接创建、复用与关闭事件，或服务端连接 ID 与受控代理日志；`ss -tnp` 里的 TIME_WAIT 数量只能作旁证，不能代表复用率；",{"type":45,"children":130},[131],{"text":132,"type":29},"超时：先按被测 SDK\u002FHTTP 客户端列出实际支持的超时类型、默认值与范围；对可独立配置的阶段用受控延迟验证，整体 deadline 单独记录。客户端只有总超时不自动代表回归缺陷；比较升级前后相同的超时契约，并注明无法独立观测的阶段；",{"type":45,"children":134},[135],{"text":136,"type":29},"重试：为每次业务调用设置测试关联 ID，分别记录客户端发送尝试、重定向、服务端接收和业务处理事件，按关联 ID 对齐后识别重试与重放；请求计数差额只能当线索，并发、日志丢失、取消和代理重放都会让它失真。",{"type":25,"children":138},[139],{"text":140,"type":29},"若超时类型或默认值改变，整体等待时间可能随之变化；应按实际客户端支持项逐一核对。",{"type":25,"children":142},[143],{"text":144,"type":29},"TLS 行为也可能随底层库变化。下面命令检查直连时的协商结果：",{"type":25,"children":146},[147],{"text":148,"type":29},"```bash\nopenssl s_client -connect api.example.com:443 -servername api.example.com \u003C\u002Fdev\u002Fnull 2>\u002Fdev\u002Fnull | grep -E 'Protocol|Cipher|verify'\n```",{"type":25,"children":150},[151],{"text":152,"type":29},"这条命令默认直连目标，只能观察直连路径上的 TLS。核对 CA 校验、TLS 版本和 SNI；若要观察代理隧道，可使用 OpenSSL 1.1.0 及以上版本的 `-proxy host:port`，或客户端自身的 TLS trace。直连结果不能代表代理路径。",{"type":31,"level":32,"children":154},[155],{"text":156,"type":29},"怎么确认流式响应没有被缓冲或中断？",{"type":25,"children":158},[159],{"text":160,"type":29},"流式输出是 AI SDK 的核心能力，也是最容易在代理链路上退化的部分。",{"type":25,"children":162},[163],{"text":164,"type":29},"TCP\u002FHTTP 读取块边界可能变化，应与应用层 SSE 事件区分。重点验证解析后的事件顺序、完整性和增量到达，以及连接中断能否被识别。TTFB 与总生成时长不能单独证明缓冲。记录首个应用事件及后续事件的单调时钟，与相同请求的升级前结果或受控上游基线比较；出现延迟时先标记为疑似，再定位客户端、代理和服务端。",{"type":31,"level":32,"children":166},[167],{"text":168,"type":29},"显式配置和代理环境变量冲突时，哪个设置生效？",{"type":25,"children":170},[171],{"text":172,"type":29},"代理设置可能来自显式参数、环境变量或 SDK 默认值，实际优先级应按所用客户端和版本核对。可在隔离环境中将其中一项设为不可用代理，观察请求错误与实际出口；同时检查 `HTTP_PROXY`、`HTTPS_PROXY` 和 `NO_PROXY` 的匹配结果。不要仅凭配置文本推断请求路径。",{"type":25,"children":174},[175,177,183,185,190],{"text":176,"type":29},"Android 网络栈与代理接入的排查思路可参考",{"text":178,"type":29,"marks":179},"Android App 网络栈测试怎么做？接入代理后异常定位",[180],{"href":181,"type":182},"https:\u002F\u002Fwww.quanqiudaili.com\u002Fblog\u002Fandroid-app-proxy-network-stack-troubleshooting","link",{"text":184,"type":29},"。Node.js 环境下的 Undici 配置请以",{"text":186,"type":29,"marks":187},"Undici API 文档",[188],{"href":189,"type":182},"https:\u002F\u002Fgithub.com\u002Fnodejs\u002Fundici\u002Ftree\u002Fmain\u002Fdocs\u002Fdocs\u002Fapi",{"text":191,"type":29},"为准。",{"type":31,"level":32,"children":193},[194],{"text":195,"type":29},"FAQ",{"type":25,"children":197},[198],{"text":199,"type":29,"marks":200},"Q：依赖升级没有改代理代码，也需要回归吗？",[201],{"type":202},"bold",{"type":25,"children":204},[205],{"text":206,"type":29},"A：需要关注传递依赖和默认值变化，尤其是代理解析、重定向与重试行为。",{"type":25,"children":208},[209],{"text":210,"type":29,"marks":211},"Q：能用生产凭据跑回归测试吗？",[212],{"type":202},{"type":25,"children":214},[215],{"text":216,"type":29},"A：优先使用受限测试凭据和非生产资源，避免重试或错误配置触发真实业务操作。",{"type":25,"children":218},[219],{"text":220,"type":29,"marks":221},"Q：不同 SDK 的测试结果可以直接横向比较吗？",[222],{"type":202},{"type":25,"children":224},[225],{"text":226,"type":29},"A：只有请求、出口、超时和服务端条件一致时才有参考价值，并应注明版本差异。","",[229,419,595],{"source":6,"id":230,"documentId":231,"translationKey":231,"locale":9,"slug":232,"path":233,"_path":233,"title":234,"description":235,"excerpt":235,"image":236,"publishedAt":237,"updatedAt":238,"author":17,"tags":239,"featured":19,"sortOrder":20,"readingTime":32,"bodyType":22,"bodyBlocks":240,"bodyHtml":227},3897,"y568ae3jjjs4yuaunluzwsqs","ai-api-egress-region-network-policy-diagnosis","\u002Fblog\u002Fai-api-egress-region-network-policy-diagnosis","测试 AI API 地区切换时，如何判断变化来自网络还是服务策略","切换代理 IP 出口地区后结果变化，如何区分网络与服务策略？本文用配对测试对照链路耗时、出口身份和服务端响应，避免把地区差异当成故障。","https:\u002F\u002Fcms.quanqiudaili.com\u002Fuploads\u002Flarge_AI_API_cover_0df5f0b27f.webp","2026-11-03T03:01:09.178Z","2026-11-03T03:01:09.154Z",[],[241,245,249,253,267,271,275,279,283,287,291,295,299,303,314,318,322,332,336,340,358,362,376,386,389,395,399,405,409,415],{"type":25,"children":242},[243],{"text":244,"type":29},"换地区之后结果变了，可能是链路、出口身份或服务策略，三者要分开取证。做法是固定请求只改地区，用分阶段耗时、IP 画像和响应证据各查一层。换地区这一步通常由代理 IP 完成，所以“换了地区”和“换了 IP”要当成两个变量记录。测试限制在自有账号和测试额度内，不通过反复请求真实付费接口制造样本。",{"type":31,"level":32,"children":246},[247],{"text":248,"type":29},"地区切换后，哪些因素可能导致结果变化？",{"type":25,"children":250},[251],{"text":252,"type":29},"地区切换可能同时改变链路、出口身份和服务策略相关条件；测试时分别记录这三类证据。",{"type":41,"format":42,"children":254},[255,259,263],{"type":45,"children":256},[257],{"text":258,"type":29},"链路质量：RTT、丢包、TLS 握手、首字节时间。",{"type":45,"children":260},[261],{"text":262,"type":29},"出口身份：IP 归属、ASN、IP 类型与信誉。",{"type":45,"children":264},[265],{"text":266,"type":29},"服务策略：限流、地区可用性、账号权限、模型可用列表。",{"type":25,"children":268},[269],{"text":270,"type":29},"如果同时换地区、出口和请求条件，结果无法区分来自哪项变化，因此对照时每轮只改变一个目标变量。",{"type":31,"level":32,"children":272},[273],{"text":274,"type":29},"怎么设计只改变出口地区的对照测试？",{"type":25,"children":276},[277],{"text":278,"type":29},"固定模型、prompt、客户端、超时和重试；按测试目的只改变地区或出口，并记录实际命中的版本和时间。",{"type":25,"children":280},[281],{"text":282,"type":29},"请求次数与观察时段应按接口波动和测试预算设定；尽量在相近时间重复对照，并避免用单次结果判断地区策略。",{"type":25,"children":284},[285],{"text":286,"type":29},"对照组至少三组：原地区、目标地区、以及目标地区的第二个出口 IP。第二组用来区分“地区本身”和“这个 IP”。",{"type":31,"level":32,"children":288},[289],{"text":290,"type":29},"怎么从 DNS、连接和响应耗时判断网络问题？",{"type":25,"children":292},[293],{"text":294,"type":29},"用 curl 对照时，必须显式使用与被测 API 相同的代理、绕过和凭据配置；凭据应从受限环境注入，不写进命令历史。下列字段是从请求开始累计的时间里程碑，不是独立 TCP\u002FTLS 耗时。代理 CONNECT、代理侧 DNS、连接复用和重定向都会影响解释；跳过的阶段标记为未观测。",{"type":25,"children":296},[297],{"text":298,"type":29},"```bash\ncurl -o \u002Fdev\u002Fnull -s -x \"$PROXY_URL\" -w 'namelookup_cumulative=%{time_namelookup} connect_cumulative=%{time_connect} appconnect_cumulative=%{time_appconnect} ttfb_cumulative=%{time_starttransfer}\\n' \\\nhttps:\u002F\u002Fapi.example.com\u002Fv1\u002Fmodels\n```",{"type":25,"children":300},[301],{"text":302,"type":29},"固定客户端、目标接口和时间窗口建立自身基线，再比较阶段分布；样本不足时只记录现象。`time_connect` 与 `time_appconnect` 不能直接当作到源站的独立 TCP\u002FTLS 时长。首字节慢也可能来自上游处理。",{"type":25,"children":304},[305,307,312],{"text":306,"type":29},"若要建立可比较的地区延迟基线，可参考",{"text":308,"type":29,"marks":309},"SaaS API 地区延迟基线怎么建？请求口径与分位数",[310],{"href":311,"type":182},"https:\u002F\u002Fwww.quanqiudaili.com\u002Fblog\u002Fsaas-api-region-latency-baseline",{"text":313,"type":29},"。",{"type":31,"level":32,"children":315},[316],{"text":317,"type":29},"怎么确认出口 IP 的地区和类型？",{"type":25,"children":319},[320],{"text":321,"type":29},"对照多个 IP 数据库与目标服务看到的源 IP\u002F地区标签，并记录来源和查询时间。ASN、组织、城市和 IP 类型标签只能提供线索，不能单独证明出口的物理所在地或住宅\u002F机房属性；平台地区识别、购买地区与物理位置应分开记录。",{"type":25,"children":323},[324,326,331],{"text":325,"type":29},"同一地区用第二个出口复测。结果变化只说明出口或路径相关因素可能参与；固定账号、请求和时间窗口重复对照，再结合链路与服务端证据判断。来源冲突时标记为不确定。相关归因思路可参考",{"text":327,"type":29,"marks":328},"多地区测试发现问题，如何判断是网络故障还是产品缺陷？",[329],{"href":330,"type":182},"https:\u002F\u002Fwww.quanqiudaili.com\u002Fblog\u002Fregional-network-vs-product-defect",{"text":313,"type":29},{"type":31,"level":32,"children":333},[334],{"text":335,"type":29},"哪些响应信息能提示账号或地区策略限制？",{"type":25,"children":337},[338],{"text":339,"type":29},"结合错误体、限流头、账号\u002F模型可用范围和传输记录判断；即使链路不稳定，有明确来源的服务端错误仍可作为证据，但不应单独结束排查。下表列出可继续核验的线索：",{"type":41,"format":42,"children":341},[342,346,350,354],{"type":45,"children":343},[344],{"text":345,"type":29},"证据：429 与 Retry-After ｜ 候选方向：限流或配额 ｜ 下一步核对：查限流头、控制台用量及重复样本",{"type":45,"children":347},[348],{"text":349,"type":29},"证据：403 与地区或模型提示 ｜ 候选方向：可用范围 ｜ 下一步核对：核对服务方文档、账号和模型配置",{"type":45,"children":351},[352],{"text":353,"type":29},"证据：request ID 与限流 header ｜ 候选方向：服务端处理 ｜ 下一步核对：按服务方定义与网关日志核验",{"type":45,"children":355},[356],{"text":357,"type":29},"证据：链路指标稳定但持续失败 ｜ 候选方向：服务策略候选 ｜ 下一步核对：重复请求并核对错误体和账号范围",{"type":31,"level":32,"children":359},[360],{"text":361,"type":29},"确认原因后，下一步分别怎么处理？",{"type":41,"format":42,"children":363},[364,368,372],{"type":45,"children":365},[366],{"text":367,"type":29},"候选方向：链路异常 ｜ 对应证据：阶段耗时反复偏离自身基线 ｜ 下一步动作：换路径或时段复测，再看服务端响应",{"type":45,"children":369},[370],{"text":371,"type":29},"候选方向：出口相关 ｜ 对应证据：固定其余条件后结果随出口变化 ｜ 下一步动作：增加配对样本，核对路径与出口来源",{"type":45,"children":373},[374],{"text":375,"type":29},"候选方向：服务策略候选 ｜ 对应证据：错误体、账号\u002F模型范围和重复样本相互吻合 ｜ 下一步动作：核对配置或向服务方确认",{"type":25,"children":377},[378,380,385],{"text":379,"type":29},"证据不一致时，记录缺少的对照条件并保持未决。curl 参数和时间字段的含义可查",{"text":381,"type":29,"marks":382},"curl 手册",[383],{"href":384,"type":182},"https:\u002F\u002Fcurl.se\u002Fdocs\u002Fmanpage.html",{"text":313,"type":29},{"type":31,"level":32,"children":387},[388],{"text":195,"type":29},{"type":25,"children":390},[391],{"text":392,"type":29,"marks":393},"Q：测试时 API 返回内容不稳定，会影响地区归因吗？",[394],{"type":202},{"type":25,"children":396},[397],{"text":398,"type":29},"A：会。应固定模型、输入和采样窗口；无法固定时，将内容波动作为单独限制记录。",{"type":25,"children":400},[401],{"text":402,"type":29,"marks":403},"Q：同一地区的两个出口结果不同，能直接认定是 IP 信誉吗？",[404],{"type":202},{"type":25,"children":406},[407],{"text":408,"type":29},"A：不能，先排除网络路径、账号状态和上游时间差，再把结论标为待验证。",{"type":25,"children":410},[411],{"text":412,"type":29,"marks":413},"Q：地区策略由服务方临时调整时，旧测试结论还有效吗？",[414],{"type":202},{"type":25,"children":416},[417],{"text":418,"type":29},"A：只对当时的账号、地区和时间窗口有效，策略变化后应重新验证。",{"source":6,"id":420,"documentId":421,"translationKey":421,"locale":9,"slug":422,"path":423,"_path":423,"title":424,"description":425,"excerpt":425,"image":426,"publishedAt":427,"updatedAt":428,"author":17,"tags":429,"featured":19,"sortOrder":20,"readingTime":32,"bodyType":22,"bodyBlocks":430,"bodyHtml":227},3896,"a6t066253yf747tqsxn0ts4x","http-client-proxy-retry-duplicate-submit-test","\u002Fblog\u002Fhttp-client-proxy-retry-duplicate-submit-test","HTTP 客户端升级后，如何检查代理重试是否导致重复提交","HTTP 客户端升级后，如何检查重试是否导致重复提交？本文用请求计数区分自动重试、重定向重放和连接中断，并说明幂等验证要点。","https:\u002F\u002Fcms.quanqiudaili.com\u002Fuploads\u002Flarge_HTTP_cover_6e441939e4.webp","2026-11-03T03:00:50.977Z","2026-11-03T03:00:50.965Z",[],[431,435,439,443,457,461,465,469,473,491,495,505,509,513,517,531,535,539,543,547,551,562,565,571,575,581,585,591],{"type":25,"children":432},[433],{"text":434,"type":29},"重复提交往往不是业务代码写错，而是客户端在代理 IP 链路上自动重试了非幂等请求。要验证这一点，得让“请求被执行了几次”变成可观测的数字，不靠肉眼核对结果。下面用一个计数接口做靶子，配合注入失败来暴露重试路径。",{"type":31,"level":32,"children":436},[437],{"text":438,"type":29},"怎么判断请求被重复处理了？",{"type":25,"children":440},[441],{"text":442,"type":29},"业务代码调用一次后，客户端可能发送多次网络请求；这不等于业务副作用也执行多次。对照三类记录：客户端发送尝试、服务端接收次数、同一业务键对应的实际副作用次数。再检查幂等键是否一致。只看最终结果不够，因为重复写入可能互相覆盖而不显错。常见重放入口如下：",{"type":41,"format":42,"children":444},[445,449,453],{"type":45,"children":446},[447],{"text":448,"type":29},"重试机制：状态码重试 ｜ 触发条件：503、502、连接超时 ｜ 注入方式：让服务端对部分请求返回 503",{"type":45,"children":450},[451],{"text":452,"type":29},"重试机制：重定向重放 ｜ 触发条件：307、308 通常要求保留方法与请求体 ｜ 注入方式：测试网关让 A 重定向到 B，仅跳转一次；循环作为单独负例并限制跳数",{"type":45,"children":454},[455],{"text":456,"type":29},"重试机制：连接重置重试 ｜ 触发条件：客户端认为请求未发出 ｜ 注入方式：代理侧主动断开连接",{"type":31,"level":32,"children":458},[459],{"text":460,"type":29},"怎么搭建一个可观察请求次数的测试接口？",{"type":25,"children":462},[463],{"text":464,"type":29},"测试接口按业务键统计接收次数和副作用次数，并返回请求序号。日志只保留脱敏关联键、必要状态和计数，过滤认证头、Cookie、URL 凭据与业务正文。测试请求应经过被测客户端和代理路径，以便对照客户端发送、服务端接收及业务处理记录。",{"type":31,"level":32,"children":466},[467],{"text":468,"type":29},"客户端会在什么情况下自动重试？",{"type":25,"children":470},[471],{"text":472,"type":29},"客户端的重试策略由具体库、版本、状态码和请求配置决定，不能只按 HTTP 方法推断。下表列的是需要核验的风险：",{"type":41,"format":42,"children":474},[475,479,483,487],{"type":45,"children":476},[477],{"text":478,"type":29},"方法：GET \u002F HEAD ｜ 方法语义：幂等 ｜ 待核验的风险：通常可安全重试，仍需核对客户端策略与服务端实现",{"type":45,"children":480},[481],{"text":482,"type":29},"方法：PUT \u002F DELETE ｜ 方法语义：幂等 ｜ 待核验的风险：客户端可能重试，需确认服务端确实做过幂等实现",{"type":45,"children":484},[485],{"text":486,"type":29},"方法：POST ｜ 方法语义：非幂等 ｜ 待核验的风险：重试即可能重复创建，需核对客户端策略、幂等键与服务端处理",{"type":45,"children":488},[489],{"text":490,"type":29},"方法：PATCH ｜ 方法语义：非幂等 ｜ 待核验的风险：部分客户端仍会重试，需按具体版本核对",{"type":25,"children":492},[493],{"text":494,"type":29},"升级后要特别确认：新版本是否把 POST 也纳入了可重试范围。这正是重复提交最常见的来源。",{"type":25,"children":496},[497,499,504],{"text":498,"type":29},"需要继续核对投递、签名与重试行为，可参考",{"text":500,"type":29,"marks":501},"SaaS Webhook 地区测试怎么做？到达、签名与重试",[502],{"href":503,"type":182},"https:\u002F\u002Fwww.quanqiudaili.com\u002Fblog\u002Fsaas-webhook-delivery-retry-region-testing",{"text":313,"type":29},{"type":31,"level":32,"children":506},[507],{"text":508,"type":29},"307 和 308 为什么可能重复发送请求？",{"type":25,"children":510},[511],{"text":512,"type":29},"客户端若自动跟随 307\u002F308，通常会尝试保留方法和请求体重新发送，但请求体是否可重放、原请求是否已被处理、服务端是否有副作用，都取决于客户端实现与应用设计。",{"type":25,"children":514},[515],{"text":516,"type":29},"测试时检查客户端是否自动跟随、请求体能否重放，以及 A、B 两端分别收到几次请求：",{"type":41,"format":42,"children":518},[519,523,527],{"type":45,"children":520},[521],{"text":522,"type":29},"是否携带同一个幂等键；",{"type":45,"children":524},[525],{"text":526,"type":29},"是否重复执行了创建逻辑；",{"type":45,"children":528},[529],{"text":530,"type":29},"是否存在重定向循环。",{"type":25,"children":532},[533],{"text":534,"type":29},"仅在隔离的测试环境中，可将 `Location` 指向计数接口，观察客户端是否再次发送请求。请求次数增加说明发生了重放；还需检查服务端是否重复执行了业务操作。若方法或请求体发生变化，应记录具体客户端行为，并对照其版本文档。",{"type":31,"level":32,"children":536},[537],{"text":538,"type":29},"代理连接中断时，写请求会不会被重新发送？",{"type":25,"children":540},[541],{"text":542,"type":29},"断开位置决定服务端是否处理过请求。可在受控环境中分别模拟发送前失败、服务端接收后失败和业务提交后丢弃响应，再对照客户端日志、接收次数与副作用记录。连接重置本身无法说明请求是否已执行。",{"type":31,"level":32,"children":544},[545],{"text":546,"type":29},"怎么模拟失败并确认修复有效？",{"type":25,"children":548},[549],{"text":550,"type":29},"在隔离环境中逐项注入故障：发送前断开、业务提交后丢弃响应、返回 503、A 到 B 的一次 307，以及受控超时。重定向循环另作负例并限制跳数。每轮只改一个条件，记录客户端发送、服务端接收和业务副作用次数。修复后重跑相同用例；只有业务记录没有重复副作用且各层计数符合预期，才算通过。",{"type":25,"children":552},[553,555,560],{"text":554,"type":29},"307\u002F308 的 HTTP 语义见 ",{"text":556,"type":29,"marks":557},"RFC 9110",[558],{"href":559,"type":182},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc9110.html",{"text":561,"type":29},"；客户端是否自动跟随及请求体能否重放，应按具体版本验证。",{"type":31,"level":32,"children":563},[564],{"text":195,"type":29},{"type":25,"children":566},[567],{"text":568,"type":29,"marks":569},"Q：幂等键能避免所有重复写入吗？",[570],{"type":202},{"type":25,"children":572},[573],{"text":574,"type":29},"A：不能，需确认服务端确实支持幂等键，并验证键的作用范围和保留时间。",{"type":25,"children":576},[577],{"text":578,"type":29,"marks":579},"Q：读请求和写请求可以使用相同重试策略吗？",[580],{"type":202},{"type":25,"children":582},[583],{"text":584,"type":29},"A：不应默认相同；写请求需评估服务端处理状态未知时的重复执行风险。",{"type":25,"children":586},[587],{"text":588,"type":29,"marks":589},"Q：客户端没有收到响应，能否判断服务端未处理请求？",[590],{"type":202},{"type":25,"children":592},[593],{"text":594,"type":29},"A：不能。连接中断时处理状态可能未知，应通过服务端记录或业务状态核对。",{"source":6,"id":596,"documentId":597,"translationKey":597,"locale":9,"slug":598,"path":599,"_path":599,"title":600,"description":601,"excerpt":601,"image":602,"publishedAt":603,"updatedAt":604,"author":17,"tags":605,"featured":19,"sortOrder":20,"readingTime":606,"bodyType":22,"bodyBlocks":607,"bodyHtml":227},3895,"tf37dm2ee9mh0u8xbco8p5ch","go-transport-proxy-connection-regression","\u002Fblog\u002Fgo-transport-proxy-connection-regression","Go 版本或 Transport 配置变更后，如何重新检查代理连接","Go 版本或 Transport 配置变更后，如何检查代理连接？本文核对代理配置来源，并对照连接复用、TLS、HTTP\u002F2 和分阶段耗时。","https:\u002F\u002Fcms.quanqiudaili.com\u002Fuploads\u002Flarge_Go_Transport_cover_4e51fe43de.webp","2026-11-03T03:00:34.422Z","2026-11-03T03:00:34.401Z",[],4,[608,612,616,620,634,638,642,646,650,654,658,662,666,670,674,678,688,692,696,710,714,718,722,736,740,744,748,752,756,778,782,789,792,798,802,808,812,818],{"type":25,"children":609},[610],{"text":611,"type":29},"Go 的代理 IP 行为由版本、环境变量和 `Transport` 配置共同决定，任何一项变化都可能改变实际走向。回归的关键是把“代理从哪来”和“请求走到哪一步”都变成可观测的。下面用标准库的 `httptrace` 与会话复用指标做示范，测试在自有服务与测试出口上进行。",{"type":31,"level":32,"children":613},[614],{"text":615,"type":29},"升级后先确认 Go 版本和哪些配置变了",{"type":25,"children":617},[618],{"text":619,"type":29},"先记录 Go 版本、构建选项和代理环境配置，作为升级前后对照基线：",{"type":41,"format":42,"children":621},[622,626,630],{"type":45,"children":623},[624],{"text":625,"type":29},"Go 版本：`go version`，大版本升级可能改变默认 TLS 与 HTTP\u002F2 行为；",{"type":45,"children":627},[628],{"text":629,"type":29},"GOFLAGS 与构建标签：是否启用或关闭了某些默认行为；",{"type":45,"children":631},[632],{"text":633,"type":29},"环境变量：`HTTP_PROXY`、`HTTPS_PROXY`、`NO_PROXY` 是否被设置，以及大小写写法。",{"type":25,"children":635},[636],{"text":637,"type":29},"`HTTP_PROXY` 与 `http_proxy` 的处理及 `NO_PROXY` 匹配规则应按 Go 版本核实。回归前记录变量是否设置、脱敏后的主机\u002F端口与绕过规则，再和升级前记录对比。不要输出含 URL userinfo、口令或完整凭据的代理环境变量。",{"type":31,"level":32,"children":639},[640],{"text":641,"type":29},"Go 请求最终使用了哪种代理配置？",{"type":25,"children":643},[644],{"text":645,"type":29},"最终用哪套配置，以当前请求实际使用的 `Transport` 和绕过规则为准。除了 `Proxy`，还要检查自定义 `DialContext`、`NO_PROXY`、重定向和连接池；环境变量是否被读取还取决于读取时机，不能只看代码或环境变量就下判断。",{"type":25,"children":647},[648],{"text":649,"type":29},"```go\ntr := &http.Transport{\nProxy: http.ProxyFromEnvironment, \u002F\u002F 读环境变量\n\u002F\u002F Proxy: http.ProxyURL(proxyURL), \u002F\u002F 显式指定\nTLSHandshakeTimeout: 5 * time.Second,\n}\nclient := &http.Client{Transport: tr, Timeout: 30 * time.Second}\nreq, _ := http.NewRequest(\"GET\", \"https:\u002F\u002Fapi.example.com\u002Fv1\u002Fmodels\", nil)\nreq.Header.Set(\"Authorization\", \"Bearer \"+os.Getenv(\"API_KEY\"))\nresp, err := client.Do(req)\n```",{"type":25,"children":651},[652],{"text":653,"type":29},"若 `Transport.Proxy` 显式设置了代理函数或地址，请求将按该配置处理，不会再由另一套环境变量配置替代。使用标准库 `ProxyFromEnvironment` 时，代理环境配置存在进程级缓存行为；按目标 Go 版本核对实现。修改环境变量后，应在设置好环境的新进程中测试，只新建 Transport 不保证刷新缓存。需要运行时动态选择代理时，用明确管理的 `Proxy` 函数或 `ProxyURL`，并单独管理连接池。",{"type":25,"children":655},[656],{"text":657,"type":29},"在隔离测试环境中，可将代理地址指向不可用端口作为负向测试。请求仍成功只说明该请求没有依赖这个代理配置；还应排查 `NO_PROXY`、其他代理设置、连接复用等因素，并核对最终出口。",{"type":25,"children":659},[660],{"text":661,"type":29},"`NO_PROXY` 命中时请求会绕过代理；应把匹配结果与实际出口一起核对。",{"type":31,"level":32,"children":663},[664],{"text":665,"type":29},"怎么用 `httptrace` 定位连接耗时？",{"type":25,"children":667},[668],{"text":669,"type":29},"`httptrace` 提供的是阶段事件，不是现成的耗时报表；需要在事件回调里记时间戳、算差值，再和总耗时对照。",{"type":25,"children":671},[672],{"text":673,"type":29},"```go\nstart := time.Now()\nvar dnsStart, connStart, tlsStart time.Time\ntrace := &httptrace.ClientTrace{\nDNSStart:          func(httptrace.DNSStartInfo) { dnsStart = time.Now() },\nDNSDone:           func(httptrace.DNSDoneInfo) { log.Printf(\"dns %v\", time.Since(dnsStart)) },\nConnectStart:      func(network, addr string) { connStart = time.Now(); log.Printf(\"connect start %s %s\", network, addr) },\nConnectDone:       func(network, addr string, err error) { log.Printf(\"connect done %s %v %v\", addr, time.Since(connStart), err) },\nTLSHandshakeStart: func() { tlsStart = time.Now() },\nTLSHandshakeDone: func(cs tls.ConnectionState, err error) {\nlog.Printf(\"tls %v proto=%s\", time.Since(tlsStart), cs.NegotiatedProtocol)\n},\nGotFirstResponseByte: func() { log.Printf(\"ttfb %v\", time.Since(start)) },\n}\nreq = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))\n```",{"type":25,"children":675},[676],{"text":677,"type":29},"开始事件与完成事件要成对记录，才能计算 DNS、拨号、TLS 等阶段时长；只注册起点不会得到 duration。示例中的单一时间变量只适用于串行、单次尝试；Happy Eyeballs 等情况下可能并发或多次触发回调，应按连接尝试关联时间戳，并用互斥锁或并发安全结构保护。若连接复用，可能没有新的拨号事件，缺事件不等于零耗时。代理下 `ConnectStart` 的目标通常是代理地址；经 HTTP 代理时 DNS 也可能由代理完成。",{"type":25,"children":679},[680,682,687],{"text":681,"type":29},"移动应用场景的 DNS、连接和 TLS 分阶段定位方法，可参考",{"text":683,"type":29,"marks":684},"移动 App 网络兼容测试：五个失败阶段定位法",[685],{"href":686,"type":182},"https:\u002F\u002Fwww.quanqiudaili.com\u002Fblog\u002Fmobile-app-network-egress-compatibility-testing",{"text":313,"type":29},{"type":31,"level":32,"children":689},[690],{"text":691,"type":29},"怎么确认连接复用和超时设置仍然生效？",{"type":25,"children":693},[694],{"text":695,"type":29},"检查连接池配置与 `GotConn` 事件，比较同一负载下的复用结果：",{"type":41,"format":42,"children":697},[698,702,706],{"type":45,"children":699},[700],{"text":701,"type":29},"`DisableKeepAlives: true` 会让每个请求重新建连；",{"type":45,"children":703},[704],{"text":705,"type":29},"`TLSHandshakeTimeout` 过短会在跨境链路上频繁失败；",{"type":45,"children":707},[708],{"text":709,"type":29},"`MaxIdleConnsPerHost` 过低会导致高并发时连接被丢弃重建。",{"type":25,"children":711},[712],{"text":713,"type":29},"观测方式：看 `httptrace.GotConn` 回调里 `info.Reused` 的取值，统计连续请求中的复用比例。复用比例受请求模式、连接池和服务端行为影响，不设统一阈值；将升级前后相同负载下的结果对照，异常时再检查配置和默认值。",{"type":31,"level":32,"children":715},[716],{"text":717,"type":29},"升级后 HTTP\u002F2 或 TLS 协商有什么变化？",{"type":25,"children":719},[720],{"text":721,"type":29},"Go 版本升级可能改变 HTTP\u002F2 启用条件和 TLS 默认值，判断时以 Go 客户端自己的结果为准。",{"type":41,"format":42,"children":723},[724,728,732],{"type":45,"children":725},[726],{"text":727,"type":29},"自定义 `TLSClientConfig` 可能关闭 HTTP\u002F2；",{"type":45,"children":729},[730],{"text":731,"type":29},"`ForceAttemptHTTP2` 的设置会影响协商结果；",{"type":45,"children":733},[734],{"text":735,"type":29},"最低 TLS 版本提升后，旧代理或中间设备可能握手失败。",{"type":25,"children":737},[738],{"text":739,"type":29},"以 Go 的 `resp.Proto`、`resp.TLS.NegotiatedProtocol`（TLS 响应时）及客户端日志作为 Go 实际协商结果。curl 只能作显式相同代理、目标和路径下的独立对照，不能代表 Go 客户端结果：",{"type":25,"children":741},[742],{"text":743,"type":29},"```bash\ncurl -sI -o \u002Fdev\u002Fnull -w '%{http_version}\\n' https:\u002F\u002Fapi.example.com\u002F\n```",{"type":25,"children":745},[746],{"text":747,"type":29},"该命令只显示 curl 自己的协商结果；Go 的结果以 `resp.Proto` 与 TLS 状态为准。两者不一致时，先确认代理、目标和请求路径相同，再检查 Go 版本、`tls.Config` 与代理隧道能力。",{"type":31,"level":32,"children":749},[750],{"text":751,"type":29},"怎么判断代理连接回归通过？",{"type":25,"children":753},[754],{"text":755,"type":29},"可按下表对照代理来源、分阶段耗时、连接复用、协议协商及超时重试。",{"type":41,"format":42,"children":757},[758,762,766,770,774],{"type":45,"children":759},[760],{"text":761,"type":29},"检查项：代理来源 ｜ 观测点：检查 `Transport.Proxy`、绕过规则及实际出口 ｜ 正常表现：与预期代理配置和出口一致",{"type":45,"children":763},[764],{"text":765,"type":29},"检查项：分阶段耗时 ｜ 观测点：`httptrace` 成对事件算出的各段时长 ｜ 正常表现：各段与升级前同一量级",{"type":45,"children":767},[768],{"text":769,"type":29},"检查项：连接复用 ｜ 观测点：`GotConn` 的 `Reused` ｜ 正常表现：与升级前相同负载下的基线对照",{"type":45,"children":771},[772],{"text":773,"type":29},"检查项：Go 协商协议 ｜ 观测点：`resp.Proto` 与 TLS 状态 ｜ 正常表现：按目标服务、代理路径和升级前基线核对",{"type":45,"children":775},[776],{"text":777,"type":29},"检查项：超时与重试 ｜ 观测点：三类超时的触发日志 ｜ 正常表现：各按预期触发，次数不变",{"type":25,"children":779},[780],{"text":781,"type":29},"如果实际出口与配置不符，先检查 `Transport.Proxy` 和绕过规则；若某阶段耗时变化，检查对应的连接与超时配置；HTTP\u002F2 协商结果变化时，再核对 Go 版本、TLS 配置和代理支持情况。",{"type":25,"children":783},[784],{"text":785,"type":29,"marks":786},"Go net\u002Fhttp 官方文档",[787],{"href":788,"type":182},"https:\u002F\u002Fpkg.go.dev\u002Fnet\u002Fhttp",{"type":31,"level":32,"children":790},[791],{"text":195,"type":29},{"type":25,"children":793},[794],{"text":795,"type":29,"marks":796},"Q：环境变量代理设置会自动覆盖自定义 Transport 吗？",[797],{"type":202},{"type":25,"children":799},[800],{"text":801,"type":29},"A：取决于 `Transport.Proxy` 是否显式设置及 Go 版本行为，应以实际配置和版本文档为准。",{"type":25,"children":803},[804],{"text":805,"type":29,"marks":806},"Q：测试关闭连接复用会代表线上表现吗？",[807],{"type":202},{"type":25,"children":809},[810],{"text":811,"type":29},"A：不会。它适合隔离连接问题，结果应与默认复用配置分开记录。",{"type":25,"children":813},[814],{"text":815,"type":29,"marks":816},"Q：只在一个 Go 版本通过是否足够？",[817],{"type":202},{"type":25,"children":819},[820],{"text":821,"type":29},"A：若生产使用多个版本，应覆盖实际部署版本及升级目标版本。",[823],{"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":824,"featured":19,"sortOrder":20,"readingTime":21,"bodyType":22,"bodyBlocks":825,"bodyHtml":227},[],[826,829,832,835,846,849,852,855,878,881,884,887,890,893,896,907,910,913,916,919,922,925,928,931,934,945,948,953,956,961,964,969],{"type":25,"children":827},[828],{"text":28,"type":29},{"type":31,"level":32,"children":830},[831],{"text":35,"type":29},{"type":25,"children":833},[834],{"text":39,"type":29},{"type":41,"format":42,"children":836},[837,840,843],{"type":45,"children":838},[839],{"text":48,"type":29},{"type":45,"children":841},[842],{"text":52,"type":29},{"type":45,"children":844},[845],{"text":56,"type":29},{"type":25,"children":847},[848],{"text":60,"type":29},{"type":31,"level":32,"children":850},[851],{"text":64,"type":29},{"type":25,"children":853},[854],{"text":68,"type":29},{"type":41,"format":42,"children":856},[857,860,863,866,869,872,875],{"type":45,"children":858},[859],{"text":74,"type":29},{"type":45,"children":861},[862],{"text":78,"type":29},{"type":45,"children":864},[865],{"text":82,"type":29},{"type":45,"children":867},[868],{"text":86,"type":29},{"type":45,"children":870},[871],{"text":90,"type":29},{"type":45,"children":873},[874],{"text":94,"type":29},{"type":45,"children":876},[877],{"text":98,"type":29},{"type":25,"children":879},[880],{"text":102,"type":29},{"type":31,"level":32,"children":882},[883],{"text":106,"type":29},{"type":25,"children":885},[886],{"text":110,"type":29},{"type":25,"children":888},[889],{"text":114,"type":29},{"type":31,"level":32,"children":891},[892],{"text":118,"type":29},{"type":25,"children":894},[895],{"text":122,"type":29},{"type":41,"format":42,"children":897},[898,901,904],{"type":45,"children":899},[900],{"text":128,"type":29},{"type":45,"children":902},[903],{"text":132,"type":29},{"type":45,"children":905},[906],{"text":136,"type":29},{"type":25,"children":908},[909],{"text":140,"type":29},{"type":25,"children":911},[912],{"text":144,"type":29},{"type":25,"children":914},[915],{"text":148,"type":29},{"type":25,"children":917},[918],{"text":152,"type":29},{"type":31,"level":32,"children":920},[921],{"text":156,"type":29},{"type":25,"children":923},[924],{"text":160,"type":29},{"type":25,"children":926},[927],{"text":164,"type":29},{"type":31,"level":32,"children":929},[930],{"text":168,"type":29},{"type":25,"children":932},[933],{"text":172,"type":29},{"type":25,"children":935},[936,937,940,941,944],{"text":176,"type":29},{"text":178,"type":29,"marks":938},[939],{"href":181,"type":182},{"text":184,"type":29},{"text":186,"type":29,"marks":942},[943],{"href":189,"type":182},{"text":191,"type":29},{"type":31,"level":32,"children":946},[947],{"text":195,"type":29},{"type":25,"children":949},[950],{"text":199,"type":29,"marks":951},[952],{"type":202},{"type":25,"children":954},[955],{"text":206,"type":29},{"type":25,"children":957},[958],{"text":210,"type":29,"marks":959},[960],{"type":202},{"type":25,"children":962},[963],{"text":216,"type":29},{"type":25,"children":965},[966],{"text":220,"type":29,"marks":967},[968],{"type":202},{"type":25,"children":970},[971],{"text":226,"type":29},null,[],[]]