fix(polish): retry 不再重发「请求体写出超时」请求,杜绝重复计费 (#680)#681
Conversation
reqwest 会把请求体写出阶段的超时同时标记为 is_request()+is_timeout(), 原 match 臂先判 `is_connect() || is_request()`,会先命中并重试,与函数自身 文档(timeout 故意不重试,因服务端可能已收到并对非幂等 LLM completion 计费) 相悖,可能重发请求 → 重复 completion + 重复插入 + 双重计费。 抽出纯函数 should_retry_transient(is_connect, is_request, is_timeout) 并加 `&& !is_timeout()` 守卫,使「任何带 timeout 标记的失败都不重试」,与 net.rs 的 策略一致;附穷举真值表单测。
|
@claude 请审核这个修复:把重试判定收紧为 |
|
Claude finished @appergb's task in 2m 12s —— View job Code Review: PR #681 — 收紧重试判定,避免请求体超时重发任务清单
总结LGTM,修复方向正确,逻辑严密,测试完整,可合并。 以下是分项分析: 1. reqwest 错误分类假设是否成立 ✅假设成立。reqwest 的错误类型使用两个正交的维度来分类:
这两个维度可以同时为 true。请求体写出阶段超时时,reqwest 会同时设置 2. 其他调用路径是否需要同步 ✅
没有其他文件引用该函数,无需同步修改。 3. 与
|
| 场景 | 原代码 | 新代码 | 服务端是否收到 |
|---|---|---|---|
| TCP 握手超时 | ✅ 重试 | ❌ 不重试 | 否(安全重试) |
| Body 写出超时 | ✅ 重试(bug) | ❌ 不重试 | 不确定(危险) |
TCP 握手超时是可以安全重试的(服务端未收到),新代码选择了更保守的策略——任何 timeout 一概不重试。这是一个轻微功能退化,但考虑到:
- LLM provider 通常配置了较长的
connect_timeout,TCP 握手超时概率极低 - 保守策略避免了所有与 timeout 相关的歧义,策略更清晰
net.rs已经在另一层为通用请求处理了 connect 重试
这个取舍合理可接受,无需修改。
5. 测试覆盖评估 ✅
三个测试共同覆盖了所有 2³ = 8 种布尔组合,是真正的穷举真值表:
(connect, request, timeout) → result
(T, F, F) → T retries_connect_and_request_failures_without_timeout
(F, T, F) → T ↑
(T, T, F) → T ↑
(F, T, T) → F never_retries_when_timeout_flagged(核心回归)
(T, F, T) → F ↑
(T, T, T) → F ↑
(F, F, T) → F ↑
(F, F, F) → F does_not_retry_unrelated_failures
覆盖完整,纯函数抽取使测试无需 mock HTTP,是正确的测试策略。
结论
此修复最小化地解决了一个真实的、有明确计费影响的 bug,改动范围精准(1 个函数定义 + 1 个调用点 + 8 条测试),无副作用。建议合并。
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
User description
关联 issue
Closes #680
问题
polish.rs::send_with_transient_retry的重试 match 臂顺序问题:reqwest 会把请求体写出阶段的超时同时标记为
is_request()+is_timeout()。这类错误先命中第一个臂被重试,与该函数文档「is_timeout()故意不重试——超时时服务端可能已收到并对非幂等 LLM completion 计费」相悖,也与共享的net.rs::send_with_retry(仅is_connect())矛盾。后果:body 写出期超时可能重发请求 → 重复 completion + 重复插入文本 + 双重计费。改动(单一职责)
should_retry_transient(is_connect, is_request, is_timeout) -> bool,逻辑(is_connect || is_request) && !is_timeout,让「任何带 timeout 标记的失败都不重试」。send_with_transient_retry改为调用该函数判定。未触碰流式插入、缓存 HTTP client、provider 逻辑。
测试
npm run build+cargo test --lib全绿(434 其余单测未受影响)。PR Type
Bug fix
Description
Prevent retries on timeout-flagged errors to avoid duplicate billing
Extract retry decision into pure function should_retry_transient
Add exhaustive unit tests for retry logic
Diagram Walkthrough
flowchart LR A[send request] --> B{error?} B -->|Ok| C[success] B -->|Err| D{is_timeout?} D -->|yes| E[don't retry] D -->|no| F{is_connect or is_request?} F -->|yes| G[retry once] F -->|no| H[don't retry]File Walkthrough
polish.rs
Refine retry logic and add testssrc-tauri/src/polish.rs
should_retry_transient(is_connect, is_request,is_timeout) -> boolthat returns true only if (is_connect ||is_request) && !is_timeout
send_with_transient_retryto use the function instead of theprevious match arm