Skip to content

fix(proxy): retry upstream storage-capacity 400 failures#1345

Merged
ding113 merged 3 commits into
ding113:devfrom
Brisbanehuang:codex/retry-upstream-disk-capacity-400
Jul 22, 2026
Merged

fix(proxy): retry upstream storage-capacity 400 failures#1345
ding113 merged 3 commits into
ding113:devfrom
Brisbanehuang:codex/retry-upstream-disk-capacity-400

Conversation

@Brisbanehuang

@Brisbanehuang Brisbanehuang commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Some upstream relays report a provider-side disk-capacity failure as HTTP 400. The response body can also contain the broad invalid request text, so CCH currently classifies it as a non-retryable client error and never retries, switches provider, or trips the circuit breaker.

This change narrows that behavior: only real HTTP 400 responses whose upstream body contains both disk storage creation failed and disk free-space floor reached are reclassified as PROVIDER_ERROR, enabling the existing retry / provider-switch / circuit-breaker paths. Ordinary invalid-request 400s and synthetic fake-200 errors are left unchanged.

Problem

An upstream relay can return HTTP 400 for what is actually a provider-side storage-capacity condition (disk-free floor reached during temp-file creation). Because the same body usually also contains the substring invalid request, the default error rule (pattern: "非法请求|illegal request|invalid request", category: invalid_request) matches it first and categorizeErrorAsync returns NON_RETRYABLE_CLIENT_ERROR. As a result:

  1. The request is never retried, even though a different provider or endpoint would likely succeed.
  2. No provider failure is recorded, so the circuit breaker never sees the degraded upstream.
  3. Provider failover does not trigger, so the client receives a hard 400 for a transient, provider-side condition.

This is the same misclassification family already addressed elsewhere in the error-categorization lineage: a provider-side failure whose body text trips a broad client-error rule and gets swallowed as non-retryable.

Related Issues / PRs:

Solution

Add a narrow, two-marker detector and wire it into two places:

  1. categorizeErrorAsync (errors.ts) — insert a priority check ahead of the client-error rule matching. isRetryableUpstreamStorageCapacityError(error) returns true only when the error is a ProxyError with statusCode === 400, is not a synthetic fake-200 error (message does not start with FAKE_200_ and upstreamError.statusCodeInferred !== true), and upstreamError.body (lowercased) contains both markers. On match it returns PROVIDER_ERROR, so the existing retry / failover / circuit-breaker paths run.

  2. tryApplyReactiveRectifier (forwarder.ts) — pass the caught error into the rectifier and short-circuit ({ matched: false }) when it is a retryable storage-capacity 400. This stops a reactive rectifier (which keys off broad phrases like invalid request) from forcing a same-provider rectified retry on an upstream that is storage-full, so the request instead falls through to normal provider-switching retry.

The detector deliberately requires both stable markers so a single-phrase coincidence cannot flip an ordinary 400 into a retryable one, and it explicitly excludes synthetic fake-200 errors so a locally-inferred status cannot masquerade as a real upstream 400.

Changes

Core Changes

  • src/app/v1/_lib/proxy/errors.ts (+36)
    • New RETRYABLE_UPSTREAM_STORAGE_ERROR_MARKERS constant (disk storage creation failed, disk free-space floor reached).
    • New exported isRetryableUpstreamStorageCapacityError(error) detector.
    • New priority branch in categorizeErrorAsync (before the client-error rule check) returning PROVIDER_ERROR for the matched signature.
  • src/app/v1/_lib/proxy/forwarder.ts (+8)
    • Import the new detector.
    • tryApplyReactiveRectifier takes a new error param and short-circuits when the error is a retryable storage-capacity 400.
    • Both reactive-rectifier call sites (main path and hedge path) now pass the caught error (lastError and error respectively).

Supporting Changes (Tests)

  • tests/unit/proxy/upstream-storage-capacity-error.test.ts (+81, new) — classification unit tests: priority over the invalid_request rule, ordinary 400 stays non-retryable, both markers required, fake-200 not reinterpreted.
  • tests/unit/proxy/proxy-forwarder-retry-limit.test.ts (+63) — retry path: storage-capacity 400 retries, records failure, and switches provider.
  • tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts (+92) — hedge path: storage-capacity 400 records failure and starts an alternative provider.

Breaking Changes

None. The only signature change is to the module-private tryApplyReactiveRectifier (new required error param), and both call sites within forwarder.ts are updated. No public exports are removed or renamed, no schema/migration, no API route changes.

Testing

Automated Tests

  • New classification unit tests + regression coverage in the retry-limit and hedge-first-byte suites.
  • Targeted upstream suites: 52/52 and 50/50 passed.
  • bun run typecheck passed; bun run build passed (existing Edge Runtime warnings remain).
  • Full bun run test: 7,023 passed, 13 skipped; the 6 failures are pre-existing and reproduce on clean dev (macOS tr locale + endpoint-circuit isolation baseline tests).
  • Biome: all changed source / new test files pass targeted checks; remaining diagnostics are pre-existing and unrelated.

Manual Testing

  • Trigger an upstream 400 whose body contains both storage markers and confirm the request retries / fails over instead of returning 400 immediately.
  • Confirm an upstream 400 carrying only invalid request (no storage markers) still returns non-retryable, with no retry or breaker accounting.
  • Confirm the provider's failure counter increments for the storage-capacity 400 so the breaker can act on the degraded upstream.

Checklist

  • Target branch is dev
  • Conventional Commit format
  • No emoji in code
  • No hardcoded user-facing strings (no UI strings touched)
  • No DB schema / migration changes

Description enhanced by Claude AI

Greptile Summary

This PR fixes a misclassification where an upstream relay's storage-capacity failure reported as HTTP 400 was being swallowed as NON_RETRYABLE_CLIENT_ERROR (due to the broad invalid request rule matching the same body), preventing retries, provider failover, and circuit-breaker accounting.

  • Adds isRetryableUpstreamStorageCapacityError in errors.ts: a deliberately narrow two-marker detector (both disk storage creation failed and disk free-space floor reached required) that runs at a new priority level before the client-error rule check and returns PROVIDER_ERROR, re-enabling the full retry/failover/circuit-breaker paths.
  • Adds an early-exit in tryApplyReactiveRectifier (forwarder.ts) so the reactive rectifier (which also keys on broad phrases like invalid request) cannot force a same-provider rectified retry on a storage-full upstream.
  • All three test suites (classification unit tests, retry-limit integration, hedge-path integration) are added with complete coverage including separate fake-200 exclusion guards.

Confidence Score: 5/5

The change is narrowly scoped to a two-marker body-text detector and an early exit in the reactive rectifier; ordinary 400s and fake-200 errors are explicitly excluded by separate guards and verified by independent test cases.

The detector requires both stable upstream markers before reclassifying an error, fake-200 exclusion is tested with each guard exercised independently, and both code paths (main retry and hedge) are covered by integration tests. The only call-site change is a new required error parameter on a module-private function, and both callers are updated.

No files require special attention.

Important Files Changed

Filename Overview
src/app/v1/_lib/proxy/errors.ts Adds isRetryableUpstreamStorageCapacityError detector and inserts it at the correct priority in categorizeErrorAsync before the broad client-error rule check; logic and guard conditions are correct.
src/app/v1/_lib/proxy/forwarder.ts Adds error param to tryApplyReactiveRectifier and short-circuits when the error is a retryable storage-capacity 400; both call sites are correctly updated.
tests/unit/proxy/upstream-storage-capacity-error.test.ts New classification unit tests; correctly covers priority override, ordinary-400 non-retryability, both-markers requirement, and separate fake-200 exclusion guards (FAKE_200_ prefix and statusCodeInferred independently).
tests/unit/proxy/proxy-forwarder-retry-limit.test.ts New integration test verifies storage-capacity 400 triggers retry, records failure on the circuit breaker, and switches to a fallback provider.
tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts New hedge-path test confirms that a storage-capacity 400 on the hedge leg records a failure, bypasses the reactive rectifier, and starts an alternative provider rather than returning the 400 to the client.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Upstream returns HTTP 400] --> B{isRetryableUpstreamStorageCapacityError?}
    B -->|checks| C{statusCode == 400\nAND not FAKE_200_\nAND not statusCodeInferred}
    C -->|yes| D{body contains\nboth markers?}
    D -->|yes| E[PROVIDER_ERROR\nretry + failover + circuit breaker]
    D -->|no| F[falls through to\nNON_RETRYABLE_CLIENT_ERROR\nif rule matches]
    C -->|no - synthetic fake-200| F
    B --> G{tryApplyReactiveRectifier}
    G -->|storage-capacity 400| H[matched: false\nskip same-provider rectifier retry]
    G -->|other error| I[normal reactive\nrectifier handling]
    E --> J[forwarder retries /\nswitches provider]
    H --> J
Loading

Reviews (3): Last reviewed commit: "Merge remote-tracking branch 'origin/dev..." | Re-trigger Greptile

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

新增上游存储容量型 HTTP 400 错误检测,将其分类为 PROVIDER_ERROR,并调整转发器的 reactive rectifier、重试及供应商切换流程;测试覆盖普通重试、hedge 和分类边界。

上游存储容量错误重试

Layer / File(s) Summary
存储容量错误分类
src/app/v1/_lib/proxy/errors.ts, tests/unit/proxy/upstream-storage-capacity-error.test.ts
识别满足完整标记的上游存储容量型 400 错误,并将其分类为 PROVIDER_ERROR;普通、推断或 fake-200 错误保持客户端错误分类。
转发器重试决策
src/app/v1/_lib/proxy/forwarder.ts
将实际错误传入 reactive rectifier,并跳过对可重试上游存储容量错误的整流重试。
重试与供应商切换验证
tests/unit/proxy/proxy-forwarder-retry-limit.test.ts, tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts
验证失败记录、重试、备用供应商切换、hedge 流程及客户端错误原因排除。

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: ding113

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed 标题准确概括了核心变更:重试上游存储容量型 400 错误。
Description check ✅ Passed 描述与变更一致,详细说明了误分类问题、修复方案和测试结果。
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai
coderabbitai Bot requested a review from ding113 July 19, 2026 11:54
@github-actions github-actions Bot added bug Something isn't working area:Error Rule labels Jul 19, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces detection and handling for upstream storage-capacity failures that are incorrectly returned as HTTP 400 errors. It adds a helper function isRetryableUpstreamStorageCapacityError to identify these errors based on specific error markers in the response body, classifying them as retryable provider errors rather than non-retryable client errors. Additionally, it updates the proxy forwarder to bypass reactive rectification for these errors, ensuring they trigger provider failover and retries. Comprehensive unit tests have been added to verify this behavior. There are no review comments to address, and I have no additional feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@Brisbanehuang

Brisbanehuang commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

CI note (verified against the current dev base 30bdda8e):

  • Focused regression suite passes locally (52/52 after the review follow-up in 68954f27), and typecheck passes.
  • Unit Tests: the 5 failures in tests/unit/proxy/response-handler-endpoint-circuit-isolation.test.ts reproduce unchanged on dev; they are stale onCommitted test fixtures from perf: optimize full-path concurrency and resource ownership #1341, not this PR.
  • Integration Tests: the 2 failures in tests/integration/proxy-hedge-lifecycle.test.ts also reproduce unchanged on dev; the assertions/mocks do not accept/invoke the current third { onCommitted } argument from perf: optimize full-path concurrency and resource ownership #1341.
  • Test Summary is only the aggregate failure from those two jobs.
  • Codex PR Review fails before review because openai/codex-action requires the PR actor to have write access, while this fork contributor has read access. The configured Claude fallback completed successfully and reported no issues.

Evidence: PR test run, dev baseline run, Codex permission failure, successful fallback review.

These failures are unrelated to the storage-capacity classification path, so I am leaving the baseline test/workflow fixes out of this PR.

@Brisbanehuang
Brisbanehuang marked this pull request as ready for review July 19, 2026 12:09
@github-actions github-actions Bot added the size/M Medium PR (< 500 lines) label Jul 19, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

This is a narrow, well-scoped carve-out that correctly classifies a known upstream storage-capacity 400 as PROVIDER_ERROR ahead of the broad invalid request client-error rule, with a matching guard in the reactive rectifier. The logic is sound, the two-marker requirement prevents ordinary 400s from flipping retryable, the fake-200 exclusion is correct, and the forwarder short-circuit is necessary rather than redundant (validated against thinking-signature-rectifier.ts, whose /invalid request/i trigger would otherwise force a wasteful same-provider retry on a storage-full upstream). No issues met the reporting threshold.

PR Size: M

  • Lines changed: 280 (280 additions, 0 deletions)
  • Files changed: 5

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 0 0
Security 0 0 0 0
Error Handling 0 0 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

Review Coverage

  • Logic and correctness - The detector's precedence branch in categorizeErrorAsync is correctly placed after transport/client-abort checks and before isNonRetryableClientErrorAsync; both forwarder call sites pass the caught error and the { matched: false } short-circuit falls through cleanly to PROVIDER_ERROR retry/switch.
  • Security (OWASP Top 10) - Marker matching uses case-insensitive String.includes, not regex; no injection surface; no secrets.
  • Error handling - No new try/catch, promises, or fallbacks introduced; no silent-failure paths added.
  • Type safety - as const marker tuple; error: Error param with internal instanceof ProxyError guard; body != null guard before .includes; no any.
  • Documentation accuracy - Comments explain the narrowness rationale and the fake-200 exclusion; no drift.
  • Test coverage - 4 classification unit tests (priority over invalid-request rule, ordinary 400 stays non-retryable, both-markers-required, fake-200 not reinterpreted) plus retry-limit and hedge-first-byte path tests asserting retry + failure recording + provider switch.
  • Code clarity - Detector is small and self-documenting; rectifier guard is a single early return.

Automated review by Claude AI

Comment thread src/app/v1/_lib/proxy/errors.ts
Comment thread tests/unit/proxy/upstream-storage-capacity-error.test.ts
# Conflicts:
#	src/app/v1/_lib/proxy/errors.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bf73fb3a60

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

]);
});

test("upstream storage-capacity 400 should retry, record failure, and switch provider", async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset the error-category mock before this test

When this suite runs in order, the preceding local-DB test sets categorizeErrorAsync with mockResolvedValue(5 as ErrorCategory), and the shared beforeEach only calls vi.clearAllMocks(), which preserves that mock implementation. This new storage-capacity test never restores the mock to PROVIDER_ERROR, so it enters the forwarder with the stale LOCAL_OVERLOAD category, skips the provider-error branch, and the recordFailure/failed-provider assertions won't be exercising the intended path. Set the mock explicitly in this test (or reset implementations in beforeEach) so the new feature has reliable unit coverage.

AGENTS.md reference: AGENTS.md:L13-L13

Useful? React with 👍 / 👎.

@ding113
ding113 merged commit 54a718c into ding113:dev Jul 22, 2026
11 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Claude Code Hub Roadmap Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Error Rule bug Something isn't working size/M Medium PR (< 500 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants