Skip to content

feat: customizable automatic retry status codes - #2663

Merged
Calcium-Ion merged 1 commit into
QuantumNous:mainfrom
seefs001:feature/retry-status-code
Jan 21, 2026
Merged

feat: customizable automatic retry status codes#2663
Calcium-Ion merged 1 commit into
QuantumNous:mainfrom
seefs001:feature/retry-status-code

Conversation

@seefs001

@seefs001 seefs001 commented Jan 14, 2026

Copy link
Copy Markdown
Collaborator

#2659

Summary by CodeRabbit

Release Notes

  • New Features

    • Configure which HTTP status codes automatically trigger retries in operation settings
    • Enhanced status code input UI with real-time validation, helpful error messages, and visual status code display
  • Documentation

    • Added English and Chinese language support for auto-retry status code configuration

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 14, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR introduces configurable automatic retry status codes functionality that allows operators to define HTTP status code ranges that should trigger retry logic. The feature extends existing status code range parsing infrastructure with new retry-specific handling, wires configuration through controller options, and adds UI components for managing these settings.

Changes

Cohort / File(s) Summary
Status code range logic
setting/operation_setting/status_code_ranges.go
Introduces new AutomaticRetryStatusCodeRanges variable with corresponding API: AutomaticRetryStatusCodesToString(), AutomaticRetryStatusCodesFromString(), and ShouldRetryByStatusCode(). Adds shared helpers statusCodeRangesToString() and shouldMatchStatusCodeRanges() to reduce duplication in range parsing and matching logic.
Status code tests
setting/operation_setting/status_code_ranges_test.go
Adds test coverage for new retry behavior via TestShouldRetryByStatusCode and TestShouldRetryByStatusCode_DefaultMatchesLegacyBehavior, validating retry decisions for various status codes against configured ranges.
Controller and model updates
controller/option.go, model/option.go
Extends UpdateOption switch with new case for "AutomaticRetryStatusCodes" that parses HTTP status code ranges. Updates InitOptionMap and updateOptionMap to handle the new retry status codes key symmetrically with existing disable codes.
Relay retry logic
controller/relay.go
Refactors shouldRetry function to replace explicit per-code retry branches with centralized ShouldRetryByStatusCode() call, while maintaining existing early-exit conditions and numeric code bounds checking.
Frontend UI component
web/src/components/settings/HttpStatusCodeRulesInput.jsx
New React component that renders a Semi UI form input with conditional tag display for parsed tokens or error messaging, providing reusable UI for status code rule configuration.
Frontend page and settings
web/src/pages/Setting/Operation/SettingsMonitoring.jsx, web/src/components/settings/OperationSetting.jsx
Adds AutomaticRetryStatusCodes state management and parsing logic, integrates new HttpStatusCodeRulesInput component for both retry and disable status code inputs, replaces direct UI element usage with the new component.
Internationalization
web/src/i18n/locales/en.json, web/src/i18n/locales/zh.json
Adds translation keys for "Auto-retry status codes" and "Invalid auto-retry status code format" in both English and Chinese, mirroring the pattern used for disable status codes.

Sequence Diagram(s)

sequenceDiagram
    participant User as User Interface
    participant Controller as Controller
    participant OptionMap as Option Map
    participant Relay as Relay Handler

    User->>Controller: POST /api/option (AutomaticRetryStatusCodes)
    Controller->>OptionMap: Parse and store AutomaticRetryStatusCodes
    OptionMap->>OptionMap: Update AutomaticRetryStatusCodeRanges
    Controller-->>User: Success response

    Note over Relay: Request arrives with error
    Relay->>Relay: Extract HTTP status code
    Relay->>Relay: Call ShouldRetryByStatusCode(code)
    Relay->>Relay: Match code against AutomaticRetryStatusCodeRanges
    alt Code matches configured range
        Relay->>Relay: Retry = true
    else Code does not match
        Relay->>Relay: Retry = false
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Hoppity-hop, requests retry with care,
Status codes parsed in ranges fair,
From UI to config, the settings flow,
Now retries dance where error codes go! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: customizable automatic retry status codes' directly and clearly describes the main change: adding configurable retry status codes, which is implemented across controller, model, setting, and web UI components.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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 and usage tips.

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/src/components/settings/OperationSetting.jsx (1)

33-80: Ensure the setInputs() merge strategy preserves defaults when API response is incomplete.

The backend currently returns AutomaticRetryStatusCodes via /api/option/, but the frontend's setInputs(newInputs) approach (line 97) replaces the entire state, causing any missing keys from the response to become undefined. While the backend initializes this key in the OptionMap and includes it in the API response, this implementation is fragile to future changes or partial responses.

The proposed defensive fix is sound:

Recommended change (merge defaults on refresh)
-      setInputs(newInputs);
+      setInputs((prev) => ({ ...prev, ...newInputs }));

This preserves frontend defaults if the backend ever omits a key during updates or migrations.

Also applies to: 84-100

🤖 Fix all issues with AI agents
In `@setting/operation_setting/status_code_ranges_test.go`:
- Around line 54-79: Tests mutate the global AutomaticRetryStatusCodeRanges
which will race if tests run in parallel; protect access by introducing a
package-level sync.RWMutex (e.g., automaticRetryMu) and update reads/writes:
wrap reads in ShouldRetryByStatusCode with a RLock/RUnlock and wrap assignments
(including in tests) with Lock/Unlock, or alternatively stop mutating the global
by providing a setter/getter that uses the mutex and update the tests to call
the setter and restore original via the getter; this ensures safe concurrent
access to AutomaticRetryStatusCodeRanges without relying on test ordering.

In `@setting/operation_setting/status_code_ranges.go`:
- Around line 17-27: AutomaticRetryStatusCodeRanges and
AutomaticDisableStatusCodeRanges are exported mutable globals causing a data
race and a broken sortedness invariant; add a package-level sync.RWMutex (e.g.,
statusCodeRangesMu) and use it to protect all reads/writes of these globals
(acquire write lock in SetAutomaticRetryStatusCodes and
SetAutomaticDisableStatusCodes, acquire read lock in ShouldDisableByStatusCode
and any readers). Also ensure setters validate/sort the slices (or store them in
a normalized form) so the sortedness invariant holds, and make
shouldMatchStatusCodeRanges robust to unsorted input by either sorting a copy
under the lock or by scanning all ranges instead of bailing early when code <
r.Start; update the SetAutomaticRetryStatusCodes/SetAutomaticDisableStatusCodes
and shouldMatchStatusCodeRanges implementations accordingly.

In `@web/src/components/settings/HttpStatusCodeRulesInput.jsx`:
- Around line 60-67: The current conditional {!parsed?.ok && (...)} shows errors
when parsed is null/undefined; update the guard to only render the error block
when parsed is present and indicates a real validation failure (for example:
parsed != null && parsed.ok === false) and ensure you access
parsed.invalidTokens safely (e.g., parsed.invalidTokens?.length) so
invalidText/invalidTokens are only shown when parsed exists and has invalid
tokens; change the conditional around the Text component (and any uses of
parsed.invalidTokens) accordingly.
🧹 Nitpick comments (1)
setting/operation_setting/status_code_ranges.go (1)

50-57: Recommend copying before publishing parsed ranges to avoid accidental external mutation.

Even though ParseHTTPStatusCodeRanges returns a fresh slice today, copying on assignment makes the “treat as immutable” intent clearer and safer.

Proposed change
 func AutomaticRetryStatusCodesFromString(s string) error {
 	ranges, err := ParseHTTPStatusCodeRanges(s)
 	if err != nil {
 		return err
 	}
-	AutomaticRetryStatusCodeRanges = ranges
+	AutomaticRetryStatusCodeRanges = append([]StatusCodeRange(nil), ranges...)
 	return nil
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6169f46 and 4bffc24.

📒 Files selected for processing (10)
  • controller/option.go
  • controller/relay.go
  • model/option.go
  • setting/operation_setting/status_code_ranges.go
  • setting/operation_setting/status_code_ranges_test.go
  • web/src/components/settings/HttpStatusCodeRulesInput.jsx
  • web/src/components/settings/OperationSetting.jsx
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/zh.json
  • web/src/pages/Setting/Operation/SettingsMonitoring.jsx
🧰 Additional context used
🧬 Code graph analysis (5)
model/option.go (2)
common/constants.go (1)
  • OptionMap (37-37)
setting/operation_setting/status_code_ranges.go (2)
  • AutomaticRetryStatusCodesToString (46-48)
  • AutomaticRetryStatusCodesFromString (50-57)
setting/operation_setting/status_code_ranges_test.go (1)
setting/operation_setting/status_code_ranges.go (3)
  • AutomaticRetryStatusCodeRanges (19-27)
  • StatusCodeRange (10-13)
  • ShouldRetryByStatusCode (59-61)
controller/relay.go (1)
setting/operation_setting/status_code_ranges.go (1)
  • ShouldRetryByStatusCode (59-61)
web/src/pages/Setting/Operation/SettingsMonitoring.jsx (3)
web/src/helpers/statusCodeRules.js (1)
  • parseHttpStatusCodeRules (1-48)
web/src/helpers/utils.jsx (1)
  • showError (122-151)
web/src/components/settings/HttpStatusCodeRulesInput.jsx (1)
  • HttpStatusCodeRulesInput (23-70)
controller/option.go (1)
setting/operation_setting/status_code_ranges.go (1)
  • ParseHTTPStatusCodeRanges (93-145)
🔇 Additional comments (9)
web/src/i18n/locales/zh.json (1)

1914-1915: Translations for auto-retry status codes look consistent with the existing auto-disable wording.

web/src/i18n/locales/en.json (1)

1928-1929: New en locale entries align with the new UI labels/errors.

model/option.go (1)

147-147: LGTM!

The new AutomaticRetryStatusCodes option is properly integrated into both InitOptionMap and updateOptionMap, following the same pattern as the existing AutomaticDisableStatusCodes. Error handling is correctly propagated.

Also applies to: 451-452

controller/option.go (1)

190-198: LGTM!

The validation case for AutomaticRetryStatusCodes correctly mirrors the existing AutomaticDisableStatusCodes handling. Input is validated with ParseHTTPStatusCodeRanges before persisting, ensuring invalid configurations are rejected early.

web/src/pages/Setting/Operation/SettingsMonitoring.jsx (4)

49-49: LGTM!

The default retry status codes are well-chosen. Notably excluding 400 (Bad Request), 408 (Azure timeout), 504/524 (gateway timeouts) aligns with the previous hardcoded retry logic that explicitly skipped these codes.


58-60: LGTM!

Parsing and validation logic for AutomaticRetryStatusCodes correctly mirrors the existing AutomaticDisableStatusCodes handling, ensuring consistent error reporting before submission.

Also applies to: 73-80


86-90: LGTM!

The normalizedMap approach cleanly handles normalized value substitution for both status code fields, avoiding code duplication.


258-270: LGTM!

The HttpStatusCodeRulesInput component is properly wired for the retry status codes with appropriate labels, placeholders, and validation feedback.

controller/relay.go (1)

320-327: LGTM!

The refactored shouldRetry logic is cleaner and correctly delegates to the configurable ShouldRetryByStatusCode helper. The guards for 2xx (success) and invalid status codes (<100 or >599) are sensible defaults.

Note: The shouldRetryTaskRelay function uses more granular hardcoded status code checks (including specific handling for timeouts like 504/524, 408 for Azure, and 307 redirects) compared to the new delegated approach in shouldRetry. Confirm whether these different semantics are intentional based on the distinct retry requirements for task relay operations.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +54 to +79
func TestShouldRetryByStatusCode(t *testing.T) {
orig := AutomaticRetryStatusCodeRanges
t.Cleanup(func() { AutomaticRetryStatusCodeRanges = orig })

AutomaticRetryStatusCodeRanges = []StatusCodeRange{
{Start: 429, End: 429},
{Start: 500, End: 599},
}

require.True(t, ShouldRetryByStatusCode(429))
require.True(t, ShouldRetryByStatusCode(500))
require.False(t, ShouldRetryByStatusCode(400))
require.False(t, ShouldRetryByStatusCode(200))
}

func TestShouldRetryByStatusCode_DefaultMatchesLegacyBehavior(t *testing.T) {
require.False(t, ShouldRetryByStatusCode(200))
require.False(t, ShouldRetryByStatusCode(400))
require.True(t, ShouldRetryByStatusCode(401))
require.False(t, ShouldRetryByStatusCode(408))
require.True(t, ShouldRetryByStatusCode(429))
require.True(t, ShouldRetryByStatusCode(500))
require.False(t, ShouldRetryByStatusCode(504))
require.False(t, ShouldRetryByStatusCode(524))
require.True(t, ShouldRetryByStatusCode(599))
}

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.

⚠️ Potential issue | 🟡 Minor

Global mutation in tests can become flaky if package tests are (now or later) run in parallel.

Consider either:

  • documenting “do not use t.Parallel() in this package/file because of global config”, or
  • protecting AutomaticRetryStatusCodeRanges with a package-level mutex and using it in tests + production.
🤖 Prompt for AI Agents
In `@setting/operation_setting/status_code_ranges_test.go` around lines 54 - 79,
Tests mutate the global AutomaticRetryStatusCodeRanges which will race if tests
run in parallel; protect access by introducing a package-level sync.RWMutex
(e.g., automaticRetryMu) and update reads/writes: wrap reads in
ShouldRetryByStatusCode with a RLock/RUnlock and wrap assignments (including in
tests) with Lock/Unlock, or alternatively stop mutating the global by providing
a setter/getter that uses the mutex and update the tests to call the setter and
restore original via the getter; this ensures safe concurrent access to
AutomaticRetryStatusCodeRanges without relying on test ordering.

Comment on lines +17 to +27
// Default behavior matches legacy hardcoded retry rules in controller/relay.go shouldRetry:
// retry for 1xx, 3xx, 4xx(except 400/408), 5xx(except 504/524), and no retry for 2xx.
var AutomaticRetryStatusCodeRanges = []StatusCodeRange{
{Start: 100, End: 199},
{Start: 300, End: 399},
{Start: 401, End: 407},
{Start: 409, End: 499},
{Start: 500, End: 503},
{Start: 505, End: 523},
{Start: 525, End: 599},
}

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the file in question
fd -type f status_code_ranges.go

Repository: QuantumNous/new-api

Length of output: 233


🏁 Script executed:

# Search for usage of AutomaticRetryStatusCodeRanges and AutomaticDisableStatusCodeRanges
rg "AutomaticRetryStatusCodeRanges|AutomaticDisableStatusCodeRanges" --type go -B 2 -A 2

Repository: QuantumNous/new-api

Length of output: 4279


🏁 Script executed:

# Search for operation settings updates and locking mechanisms
rg "operation.*[Ss]etting" --type go | head -30

Repository: QuantumNous/new-api

Length of output: 2793


🏁 Script executed:

# Look for "shouldRetry" and references to controller/relay.go
rg "shouldRetry|controller/relay" --type go -B 2 -A 2 | head -50

Repository: QuantumNous/new-api

Length of output: 1870


🏁 Script executed:

# Check if there's a config or settings struct with locks/atomics
rg "sync\.(Mutex|RWMutex|Atomic)" --type go -B 2 -A 2 | head -40

Repository: QuantumNous/new-api

Length of output: 2242


🏁 Script executed:

# Read the complete status_code_ranges.go file
cat setting/operation_setting/status_code_ranges.go

Repository: QuantumNous/new-api

Length of output: 4573


🏁 Script executed:

# Check for any locking mechanism specific to operation_setting package
rg "Mutex" setting/operation_setting/ --type go

Repository: QuantumNous/new-api

Length of output: 45


Major: data race and sortedness invariant in global mutable status code ranges.

  1. Data race: AutomaticRetryStatusCodeRanges and AutomaticDisableStatusCodeRanges are unprotected exported globals. They are read in request paths (e.g., service/channel.go calls ShouldDisableByStatusCode) and written via SetAutomaticRetryStatusCodes/SetAutomaticDisableStatusCodes. Concurrent reads/writes are a Go data race. Other operation settings modules (e.g., setting/ratio_setting/model_ratio.go, setting/user_usable_group.go) consistently protect mutable globals with sync.RWMutex.

  2. Sortedness invariant: shouldMatchStatusCodeRanges relies on sorted ranges—it returns false when code < r.Start, skipping later ranges. ParseHTTPStatusCodeRanges enforces sorting, but the globals can be assigned directly (as tests do), bypassing this guarantee and silently breaking the matching logic.

Fix: Add sync.RWMutex around both globals (consistent with other settings), and make shouldMatchStatusCodeRanges order-independent:

Proposed changes
-func shouldMatchStatusCodeRanges(ranges []StatusCodeRange, code int) bool {
+func shouldMatchStatusCodeRanges(ranges []StatusCodeRange, code int) bool {
 	if code < 100 || code > 599 {
 		return false
 	}
 	for _, r := range ranges {
-		if code < r.Start {
-			return false
-		}
-		if code <= r.End {
+		if code >= r.Start && code <= r.End {
 			return true
 		}
 	}
 	return false
 }

Also applies to: 35-52 (update functions), 74-84 (shouldMatchStatusCodeRanges)

🤖 Prompt for AI Agents
In `@setting/operation_setting/status_code_ranges.go` around lines 17 - 27,
AutomaticRetryStatusCodeRanges and AutomaticDisableStatusCodeRanges are exported
mutable globals causing a data race and a broken sortedness invariant; add a
package-level sync.RWMutex (e.g., statusCodeRangesMu) and use it to protect all
reads/writes of these globals (acquire write lock in
SetAutomaticRetryStatusCodes and SetAutomaticDisableStatusCodes, acquire read
lock in ShouldDisableByStatusCode and any readers). Also ensure setters
validate/sort the slices (or store them in a normalized form) so the sortedness
invariant holds, and make shouldMatchStatusCodeRanges robust to unsorted input
by either sorting a copy under the lock or by scanning all ranges instead of
bailing early when code < r.Start; update the
SetAutomaticRetryStatusCodes/SetAutomaticDisableStatusCodes and
shouldMatchStatusCodeRanges implementations accordingly.

Comment on lines +60 to +67
{!parsed?.ok && (
<Text type='danger' style={{ display: 'block', marginTop: 8 }}>
{invalidText}
{parsed?.invalidTokens && parsed.invalidTokens.length > 0
? `: ${parsed.invalidTokens.join(', ')}`
: ''}
</Text>
)}

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.

⚠️ Potential issue | 🟡 Minor

Guard against undefined parsed prop to avoid false error display.

When parsed is undefined or null (e.g., before initial parsing), !parsed?.ok evaluates to true, which will display the error text even though no actual validation error occurred.

🐛 Proposed fix
-      {!parsed?.ok && (
+      {parsed && !parsed.ok && (
         <Text type='danger' style={{ display: 'block', marginTop: 8 }}>
           {invalidText}
           {parsed?.invalidTokens && parsed.invalidTokens.length > 0
             ? `: ${parsed.invalidTokens.join(', ')}`
             : ''}
         </Text>
       )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{!parsed?.ok && (
<Text type='danger' style={{ display: 'block', marginTop: 8 }}>
{invalidText}
{parsed?.invalidTokens && parsed.invalidTokens.length > 0
? `: ${parsed.invalidTokens.join(', ')}`
: ''}
</Text>
)}
{parsed && !parsed.ok && (
<Text type='danger' style={{ display: 'block', marginTop: 8 }}>
{invalidText}
{parsed?.invalidTokens && parsed.invalidTokens.length > 0
? `: ${parsed.invalidTokens.join(', ')}`
: ''}
</Text>
)}
🤖 Prompt for AI Agents
In `@web/src/components/settings/HttpStatusCodeRulesInput.jsx` around lines 60 -
67, The current conditional {!parsed?.ok && (...)} shows errors when parsed is
null/undefined; update the guard to only render the error block when parsed is
present and indicates a real validation failure (for example: parsed != null &&
parsed.ok === false) and ensure you access parsed.invalidTokens safely (e.g.,
parsed.invalidTokens?.length) so invalidText/invalidTokens are only shown when
parsed exists and has invalid tokens; change the conditional around the Text
component (and any uses of parsed.invalidTokens) accordingly.

@seefs001 seefs001 linked an issue Jan 15, 2026 that may be closed by this pull request
5 tasks
@Calcium-Ion
Calcium-Ion merged commit c08a934 into QuantumNous:main Jan 21, 2026
1 check passed
ennnnny pushed a commit to ennnnny/new-api that referenced this pull request Mar 17, 2026
…s-code

feat: customizable automatic retry status codes
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.

exhausted没被重试

2 participants