Skip to content

fix: recover from previous_response_not_found instead of passing it through#418

Merged
icebear0828 merged 1 commit intodevfrom
fix/previous-response-not-found-recovery
Apr 27, 2026
Merged

fix: recover from previous_response_not_found instead of passing it through#418
icebear0828 merged 1 commit intodevfrom
fix/previous-response-not-found-recovery

Conversation

@icebear0828
Copy link
Copy Markdown
Owner

Summary

  • 上游返回 previous_response_not_found(response 由别的账号创建 / SessionAffinityMap 过期或重启丢失 / 跨账号轮转)时,proxy 自动剥掉 previous_response_id + turnState,在同一账号上重试一次,并把这条 ID 从 affinity map 清掉
  • 修复需要两半:先让 WS 首帧错误转成 CodexApiError reject(ws-transport.ts ROTATABLE_ERROR_CODES 增补),再在 catch 里 strip + retry(proxy-handler.ts
  • 隐式续链场景通过已有 restoreImplicitResumeRequest() 路径回放完整 input,无损恢复;显式续链丢服务端历史但请求仍能完成

Why

之前这个错误从 chatgpt.com 以 in-stream WS 错误帧到达,不在 ROTATABLE_ERROR_CODES 白名单里 → 直接被流式翻译器格式化成 [Error] previous_response_not_found: 透传给客户端。proxy-handler 的 catch 块根本没触发。客户端(Codex CLI / Claude Code 等)没法 graceful 处理这个错误,整个对话上下文丢失就挂了。

Changes

  • src/proxy/error-classification.ts — 新增 isPreviousResponseNotFoundError,按 body code + message 兜底两路识别
  • src/proxy/ws-transport.tsROTATABLE_ERROR_CODES 增补 previous_response_not_found: 400
  • src/auth/session-affinity.ts — 新增 forget(responseId) 方法
  • src/routes/shared/proxy-handler.ts — catch 块新增 strip-and-retry,复用 restoreImplicitResumeRequest(),loop guard 防死循环
  • tests/unit/proxy/ws-transport-early-error.test.ts — WS 首帧 previous_response_not_foundCodexApiError(400)
  • tests/integration/proxy-handler.test.ts — strip-retry 恢复路径 + loop guard

Test plan

  • npm test — 1619 tests pass (143 files)
  • npx tsc --noEmit — 干净
  • 真实 chatgpt.com E2E:3/3 连续成功恢复
    • 日志证实 affinity=missprevious_response_not_foundstripping and retrying same account → 200 OK
    • 测试用 Plus 账号通过 /v1/responses + 已知 stale previous_response_id 触发
  • 反向验证:白名单加入前 ws-transport 测试失败、proxy-handler 测试失败 — 加入后通过,证明两半都必需

…hrough

When the upstream rejects a request with `previous_response_not_found`
(response was created by a different account, the in-memory affinity map
was lost on restart / 4h TTL expiry, or rotation forced the request onto
a different account), the proxy now strips the stale `previous_response_id`
+ `turnState`, retries once on the same account, and forgets the stale
entry from `SessionAffinityMap` so subsequent requests aren't routed to
the same wrong account.

Two halves are required because the error reaches the proxy as an
in-stream WS frame, not as a synchronous HTTP error:

1. `ws-transport.ts:36` — add `previous_response_not_found: 400` to
   `ROTATABLE_ERROR_CODES` so the first-frame error is converted to a
   `CodexApiError` reject, letting the proxy-handler catch block run.
2. `proxy-handler.ts` — in the catch, detect via the new
   `isPreviousResponseNotFoundError` classifier, reuse the existing
   `restoreImplicitResumeRequest()` path (so implicit-resume requests
   replay the full input losslessly), then clear the stale ID + turn
   state and retry. A `prevRespNotFoundRetried` flag prevents looping
   if the second attempt also fails.

Implicit-resume scenarios recover losslessly. Explicit-resume requests
(client-supplied `previous_response_id`) lose server-side history but
still complete with the latest input — better than a hard error that
most clients can't gracefully handle.

Verified end-to-end against real chatgpt.com upstream: 3/3 successful
recoveries with `affinity=miss` + `previous_response_not_found` →
`stripping and retrying same account` → 200 OK.
@icebear0828 icebear0828 merged commit 6abf6ae into dev Apr 27, 2026
1 check passed
@icebear0828 icebear0828 deleted the fix/previous-response-not-found-recovery branch April 27, 2026 04:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant