Skip to content

feat(routing): fallback_on_statuses — opt selected upstream statuses into retry/failover - #752

Merged
jarvis9443 merged 2 commits into
mainfrom
feat/fallback-on-statuses
Jul 10, 2026
Merged

feat(routing): fallback_on_statuses — opt selected upstream statuses into retry/failover#752
jarvis9443 merged 2 commits into
mainfrom
feat/fallback-on-statuses

Conversation

@jarvis9443

@jarvis9443 jarvis9443 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Problem

Some providers use non-429 4xx codes for transient conditions — model overload, queue full, quota exhaustion. The default classification (is_retryable) treats every non-429 4xx as the caller's error and relays it, so a model group fails a request that its other targets could have served. (AISIX-Cloud#1012)

Change

Routing models gain an explicit, empty-by-default opt-in list:

routing:
  strategy: failover
  targets: [{ model: primary }, { model: backup }]
  fallback_on_statuses: [408, 409, 422]

A listed status becomes retryable in is_retryable() — the single predicate every dispatch loop consults — so it participates in both same-target retries (retries) and failover (max_fallbacks), exactly like retry_on_429 does for 429 (429 in the list works without retry_on_429; the boolean stays for compatibility). Unset = behavior unchanged, bit-for-bit. 5xx are already retryable (listing them is a no-op); the list never affects non-status failures (customer-fixable config/credential errors stay terminal). Schema constrains entries to 400–599.

Threaded through all six dispatch sites: chat streaming + non-streaming + cross-target break, messages, responses, count_tokens — the whole routing family in one PR.

Observability: per-attempt telemetry already records each attempt's upstream status, error_class and attempt_kind (initial/retry/fallback), so a rule-triggered failover is fully attributable from existing fields; no new telemetry needed.

Interaction note: this is the current-request knob. The pre-existing per-direct-model cooldown.trigger_statuses benches a target for future requests and stays an independent layer.

Deliberately not included

  • Provider error-code / response-body matchers (the issue floats error.code: Throttling-style matching as a possible extension): status-code opt-in covers the reported scenarios; body matching adds a parsing surface per provider and can be layered on later if a concrete provider needs it.
  • Changing defaults (e.g. making 408/409 retryable out of the box, as some gateways do): the issue explicitly requires unchanged defaults.

Baseline: mainstream gateways retry 408/409/429/5xx by default and let users force retries per exception category; none offer per-status opt-in. We keep stricter defaults and add the more precise status-list control instead — providers in scope here signal overload with codes (e.g. 422) that category-based systems can't distinguish from validation errors.

Control-plane pairing

The CP is a closed schema — this field is unreachable by users until the paired AISIX-Cloud PR (cp-admin.yaml + validation + etcd projection + dashboard) lands. DP ships first because it rejects unknown fields (deny_unknown_fields); the CP PR follows immediately and carries the closing reference for AISIX-Cloud#1012.

Tests

  • Unit: listed 4xx retryable; unlisted 4xx/400 stay terminal; 429-via-list without retry_on_429; 5xx unaffected; non-status failures never resurrected; existing classification test updated to the new signature with &[] (pinning default-unchanged).
  • e2e (real aisix + etcd): two identical two-target groups — default group relays the first target's 422 without consulting the backup; fallback_on_statuses: [422] group fails over and succeeds on the backup; a 400 from a configured group (422-only list) still relays. Existing fallback/fallback-edges e2es pass unchanged.

Ref api7/AISIX-Cloud#1012 (closing reference rides the CP PR so the issue doesn't close on the DP half)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added configurable HTTP status codes that can trigger upstream retries and failover.
    • Supports status codes from 400–599, with existing behavior preserved when unset.
    • Applied configurable fallback handling across chat, message, response, and token-counting requests.
  • Bug Fixes

    • Improved failover behavior for selected upstream client-error responses.
  • Tests

    • Added validation and end-to-end coverage for configured, unconfigured, and unlisted status codes.

…into retry/failover

Some providers use non-429 4xx codes for transient conditions (model
overload, queue full, quota exhaustion). The gateway's default treats
any non-429 4xx as the caller's error and relays it, so a model group
fails a request its other targets could have served
(AISIX-Cloud#1012).

Routing models gain an explicit opt-in list:

  routing:
    strategy: failover
    targets: [...]
    fallback_on_statuses: [408, 409, 422]

A listed status becomes retryable in is_retryable() — the single
predicate every dispatch loop consults — so it participates in both
same-target retries and failover, exactly like retry_on_429 does for
429. Unset means unchanged behavior. 5xx codes are already retryable;
listing them is a no-op. The list never affects non-status failures
(customer-fixable config/credentials stay terminal). Schema constrains
entries to 400-599.

Threaded through all six dispatch call sites (chat streaming +
non-streaming, messages, responses, count_tokens). Per-attempt
telemetry already records each attempt's upstream status and kind
(initial/retry/fallback), so a rule-triggered failover is attributable
from existing fields.

Control-plane exposure (cp-admin schema, validation, projection,
dashboard) ships as the paired AISIX-Cloud PR; this changes the DP
first so the CP projection lands against a DP that accepts the field
(deny_unknown_fields).

Ref api7/AISIX-Cloud#1012
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds optional fallback_on_statuses routing configuration for HTTP statuses 400–599, threads it through proxy retry decisions, and validates default, configured, and non-listed status behavior.

Changes

Configurable fallback status routing

Layer / File(s) Summary
Routing configuration and schema
crates/aisix-core/src/models/routing.rs, crates/aisix-core/src/models/schema.rs, schemas/resources/*.json
Adds optional fallback_on_statuses, validates statuses from 400 through 599, and provides an empty default slice.
Retry classification
crates/aisix-proxy/src/routing.rs
Updates is_retryable to treat configured upstream statuses as retryable while preserving terminal behavior for unlisted statuses and non-status failures.
Proxy dispatch integration
crates/aisix-proxy/src/chat.rs, crates/aisix-proxy/src/count_tokens.rs, crates/aisix-proxy/src/messages.rs, crates/aisix-proxy/src/responses.rs
Passes model-specific fallback statuses into streaming, non-streaming, messages, responses, and token-count retry/failover paths.
Fallback behavior validation
tests/e2e/src/cases/fallback-on-statuses-e2e.test.ts
Verifies default terminal handling, configured 422 failover, and terminal handling for an unlisted 400 response.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ProxyDispatch
  participant RoutingConfig
  participant Upstream
  Client->>ProxyDispatch: send request
  ProxyDispatch->>RoutingConfig: read fallback_on_statuses
  ProxyDispatch->>Upstream: try primary target
  Upstream-->>ProxyDispatch: 422 upstream error
  ProxyDispatch->>ProxyDispatch: classify status as retryable
  ProxyDispatch->>Upstream: try fallback target
  Upstream-->>ProxyDispatch: successful response
  ProxyDispatch-->>Client: return response
Loading

Possibly related PRs

  • api7/aisix#472: Refactors target resolution in the same proxy routing dispatch paths extended here with fallback-status retry decisions.
🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main routing change and the new opt-in fallback status behavior.
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.
E2e Test Quality Review ✅ Passed Real E2E flow uses spawned app, etcd, and HTTP upstreams; scenarios cover default, opt-in, and unlisted statuses with clear, independent assertions.
Security Check ✅ Passed No issues found; the PR only adds an opt-in retry-status list and threads it through dispatch, with no secret/log/auth/TLS/db code touched.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/fallback-on-statuses

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

…ange at the validator

Audit follow-ups: the six dispatch sites now borrow the configured
slice instead of cloning per request, and a schema-validation test
pins that out-of-range entries are rejected while in-range lists pass.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
crates/aisix-proxy/src/chat.rs (1)

1101-1114: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate routing-config extraction pattern; consider a Model-level helper.

fallback_statuses is derived with the same virtual_entry.value.routing.as_ref().map(|r| r.fallback_on_statuses_or_default()).unwrap_or(&[]) boilerplate twice in this file (streaming and non-streaming paths), and the identical pattern (for both retry_on_429 and now fallback_on_statuses) is repeated again in count_tokens.rs (and, per the graph context, in messages.rs/responses.rs). A small convenience method on Model (e.g. fallback_on_statuses_or_default(&self) -> &[u16], mirroring the existing Routing method) would collapse all these call sites to a single line and remove the risk of the branches drifting.

♻️ Proposed helper (illustrative)
impl Model {
    pub fn fallback_on_statuses_or_default(&self) -> &[u16] {
        self.routing
            .as_ref()
            .map(|r| r.fallback_on_statuses_or_default())
            .unwrap_or(&[])
    }
}

Then each call site becomes let fallback_statuses = virtual_entry.value.fallback_on_statuses_or_default();.

Also applies to: 1822-1833

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aisix-proxy/src/chat.rs` around lines 1101 - 1114, Introduce a
Model-level helper, such as Model::fallback_on_statuses_or_default, that
delegates to routing when present and otherwise returns an empty slice. Replace
the repeated routing.as_ref().map(...).unwrap_or(&[]) extraction in both chat.rs
paths and the corresponding count_tokens.rs, messages.rs, and responses.rs call
sites with this helper, including the related retry configuration pattern where
applicable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/aisix-proxy/src/chat.rs`:
- Around line 1101-1114: Introduce a Model-level helper, such as
Model::fallback_on_statuses_or_default, that delegates to routing when present
and otherwise returns an empty slice. Replace the repeated
routing.as_ref().map(...).unwrap_or(&[]) extraction in both chat.rs paths and
the corresponding count_tokens.rs, messages.rs, and responses.rs call sites with
this helper, including the related retry configuration pattern where applicable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cfffe33d-ca44-4bce-af17-40c31da81ac5

📥 Commits

Reviewing files that changed from the base of the PR and between 31357d1 and 5c0dfcf.

📒 Files selected for processing (10)
  • crates/aisix-core/src/models/routing.rs
  • crates/aisix-core/src/models/schema.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/responses.rs
  • crates/aisix-proxy/src/routing.rs
  • schemas/resources/model.schema.json
  • schemas/resources/routing.schema.json
  • tests/e2e/src/cases/fallback-on-statuses-e2e.test.ts

@jarvis9443
jarvis9443 merged commit c750e4c into main Jul 10, 2026
12 checks passed
@jarvis9443
jarvis9443 deleted the feat/fallback-on-statuses branch July 10, 2026 12:57
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