Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions controller/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/setting"
"github.com/QuantumNous/new-api/setting/console_setting"
"github.com/QuantumNous/new-api/setting/operation_setting"
"github.com/QuantumNous/new-api/setting/ratio_setting"
"github.com/QuantumNous/new-api/setting/system_setting"

Expand Down Expand Up @@ -177,6 +178,15 @@ func UpdateOption(c *gin.Context) {
})
return
}
case "AutomaticDisableStatusCodes":
_, err = operation_setting.ParseHTTPStatusCodeRanges(option.Value.(string))
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
case "console_setting.api_info":
err = console_setting.ValidateConsoleSettings(option.Value.(string), "ApiInfo")
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions controller/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ func processChannelError(c *gin.Context, channelError types.ChannelError, err *t
// do not use context to get channel info, there may be inconsistent channel info when processing asynchronously
if service.ShouldDisableChannel(channelError.ChannelType, err) && channelError.AutoBan {
gopool.Go(func() {
service.DisableChannel(channelError, err.Error())
service.DisableChannel(channelError, err.ErrorWithStatusCode())
})
}

Expand Down Expand Up @@ -378,7 +378,7 @@ func processChannelError(c *gin.Context, channelError types.ChannelError, err *t
adminInfo["multi_key_index"] = common.GetContextKeyInt(c, constant.ContextKeyChannelMultiKeyIndex)
}
other["admin_info"] = adminInfo
model.RecordErrorLog(c, userId, channelId, modelName, tokenName, err.MaskSensitiveError(), tokenId, 0, false, userGroup, other)
model.RecordErrorLog(c, userId, channelId, modelName, tokenName, err.MaskSensitiveErrorWithStatusCode(), tokenId, 0, false, userGroup, other)
}

}
Expand Down
3 changes: 3 additions & 0 deletions model/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ func InitOptionMap() {
common.OptionMap["SensitiveWords"] = setting.SensitiveWordsToString()
common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength)
common.OptionMap["AutomaticDisableKeywords"] = operation_setting.AutomaticDisableKeywordsToString()
common.OptionMap["AutomaticDisableStatusCodes"] = operation_setting.AutomaticDisableStatusCodesToString()
common.OptionMap["ExposeRatioEnabled"] = strconv.FormatBool(ratio_setting.IsExposeRatioEnabled())

// 自动添加所有注册的模型配置
Expand Down Expand Up @@ -444,6 +445,8 @@ func updateOptionMap(key string, value string) (err error) {
setting.SensitiveWordsFromString(value)
case "AutomaticDisableKeywords":
operation_setting.AutomaticDisableKeywordsFromString(value)
case "AutomaticDisableStatusCodes":
err = operation_setting.AutomaticDisableStatusCodesFromString(value)
case "StreamCacheQueueLength":
setting.StreamCacheQueueLength, _ = strconv.Atoi(value)
case "PayMethods":
Expand Down
5 changes: 4 additions & 1 deletion service/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,12 @@ func ShouldDisableChannel(channelType int, err *types.NewAPIError) bool {
if types.IsSkipRetryError(err) {
return false
}
if err.StatusCode == http.StatusUnauthorized {
if operation_setting.ShouldDisableByStatusCode(err.StatusCode) {
return true
}
//if err.StatusCode == http.StatusUnauthorized {
// return true
//}
if err.StatusCode == http.StatusForbidden {
switch channelType {
case constant.ChannelTypeGemini:
Expand Down
147 changes: 147 additions & 0 deletions setting/operation_setting/status_code_ranges.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package operation_setting

import (
"fmt"
"sort"
"strconv"
"strings"
)

type StatusCodeRange struct {
Start int
End int
}

var AutomaticDisableStatusCodeRanges = []StatusCodeRange{{Start: 401, End: 401}}

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:

#!/bin/bash
# Check if there's existing synchronization in the codebase for similar globals
rg -n "sync\.(RW)?Mutex" --type go -C2 | head -100

echo "---SEPARATOR---"

# Check how AutomaticDisableStatusCodeRanges is accessed across the codebase
rg -n "AutomaticDisableStatusCodeRanges" --type go

Repository: QuantumNous/new-api

Length of output: 5806


Global variable lacks thread-safety protection.

AutomaticDisableStatusCodeRanges is a mutable global that can be read by ShouldDisableByStatusCode (line 45) while being written by AutomaticDisableStatusCodesFromString (line 37). This pattern violates the synchronization pattern used throughout the codebase for similar globals (e.g., userUsableGroupsMutex, ModelRequestRateLimitMutex, modelPriceMapMutex). Protect concurrent access with sync.RWMutex.

🤖 Prompt for AI Agents
In @setting/operation_setting/status_code_ranges.go at line 15,
AutomaticDisableStatusCodeRanges is a mutable global accessed concurrently by
ShouldDisableByStatusCode (reads) and AutomaticDisableStatusCodesFromString
(writes); add a package-level sync.RWMutex (e.g.,
automaticDisableStatusCodeRangesMutex) and wrap all reads of
AutomaticDisableStatusCodeRanges in RLock/RUnlock and all writes/assignments in
Lock/Unlock, updating those two functions to use the mutex to ensure
thread-safety consistent with other globals like userUsableGroupsMutex and
modelPriceMapMutex.


func AutomaticDisableStatusCodesToString() string {
if len(AutomaticDisableStatusCodeRanges) == 0 {
return ""
}
parts := make([]string, 0, len(AutomaticDisableStatusCodeRanges))
for _, r := range AutomaticDisableStatusCodeRanges {
if r.Start == r.End {
parts = append(parts, strconv.Itoa(r.Start))
continue
}
parts = append(parts, fmt.Sprintf("%d-%d", r.Start, r.End))
}
return strings.Join(parts, ",")
}

func AutomaticDisableStatusCodesFromString(s string) error {
ranges, err := ParseHTTPStatusCodeRanges(s)
if err != nil {
return err
}
AutomaticDisableStatusCodeRanges = ranges
return nil
}

func ShouldDisableByStatusCode(code int) bool {
if code < 100 || code > 599 {
return false
}
for _, r := range AutomaticDisableStatusCodeRanges {
if code < r.Start {
return false
}
if code <= r.End {
return true
}
}
return false
}

func ParseHTTPStatusCodeRanges(input string) ([]StatusCodeRange, error) {
input = strings.TrimSpace(input)
if input == "" {
return nil, nil
}

input = strings.NewReplacer(",", ",").Replace(input)
segments := strings.Split(input, ",")

var ranges []StatusCodeRange
var invalid []string

for _, seg := range segments {
seg = strings.TrimSpace(seg)
if seg == "" {
continue
}
r, err := parseHTTPStatusCodeToken(seg)
if err != nil {
invalid = append(invalid, seg)
continue
}
ranges = append(ranges, r)
}

if len(invalid) > 0 {
return nil, fmt.Errorf("invalid http status code rules: %s", strings.Join(invalid, ", "))
}
if len(ranges) == 0 {
return nil, nil
}

sort.Slice(ranges, func(i, j int) bool {
if ranges[i].Start == ranges[j].Start {
return ranges[i].End < ranges[j].End
}
return ranges[i].Start < ranges[j].Start
})

merged := []StatusCodeRange{ranges[0]}
for _, r := range ranges[1:] {
last := &merged[len(merged)-1]
if r.Start <= last.End+1 {
if r.End > last.End {
last.End = r.End
}
continue
}
merged = append(merged, r)
}

return merged, nil
}

func parseHTTPStatusCodeToken(token string) (StatusCodeRange, error) {
token = strings.TrimSpace(token)
token = strings.ReplaceAll(token, " ", "")
if token == "" {
return StatusCodeRange{}, fmt.Errorf("empty token")
}

if strings.Contains(token, "-") {
parts := strings.Split(token, "-")
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return StatusCodeRange{}, fmt.Errorf("invalid range token: %s", token)
}
start, err := strconv.Atoi(parts[0])
if err != nil {
return StatusCodeRange{}, fmt.Errorf("invalid range start: %s", token)
}
end, err := strconv.Atoi(parts[1])
if err != nil {
return StatusCodeRange{}, fmt.Errorf("invalid range end: %s", token)
}
if start > end {
return StatusCodeRange{}, fmt.Errorf("range start > end: %s", token)
}
if start < 100 || end > 599 {
return StatusCodeRange{}, fmt.Errorf("range out of bounds: %s", token)
}
return StatusCodeRange{Start: start, End: end}, nil
}

code, err := strconv.Atoi(token)
if err != nil {
return StatusCodeRange{}, fmt.Errorf("invalid status code: %s", token)
}
if code < 100 || code > 599 {
return StatusCodeRange{}, fmt.Errorf("status code out of bounds: %s", token)
}
return StatusCodeRange{Start: code, End: code}, nil
}
52 changes: 52 additions & 0 deletions setting/operation_setting/status_code_ranges_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package operation_setting

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestParseHTTPStatusCodeRanges_CommaSeparated(t *testing.T) {
ranges, err := ParseHTTPStatusCodeRanges("401,403,500-599")
require.NoError(t, err)
require.Equal(t, []StatusCodeRange{
{Start: 401, End: 401},
{Start: 403, End: 403},
{Start: 500, End: 599},
}, ranges)
}

func TestParseHTTPStatusCodeRanges_MergeAndNormalize(t *testing.T) {
ranges, err := ParseHTTPStatusCodeRanges("500-505,504,401,403,402")
require.NoError(t, err)
require.Equal(t, []StatusCodeRange{
{Start: 401, End: 403},
{Start: 500, End: 505},
}, ranges)
}

func TestParseHTTPStatusCodeRanges_Invalid(t *testing.T) {
_, err := ParseHTTPStatusCodeRanges("99,600,foo,500-400,500-")
require.Error(t, err)
}

func TestParseHTTPStatusCodeRanges_NoComma_IsInvalid(t *testing.T) {
_, err := ParseHTTPStatusCodeRanges("401 403")
require.Error(t, err)
}

func TestShouldDisableByStatusCode(t *testing.T) {
orig := AutomaticDisableStatusCodeRanges
t.Cleanup(func() { AutomaticDisableStatusCodeRanges = orig })

AutomaticDisableStatusCodeRanges = []StatusCodeRange{
{Start: 401, End: 403},
{Start: 500, End: 599},
}

require.True(t, ShouldDisableByStatusCode(401))
require.True(t, ShouldDisableByStatusCode(403))
require.False(t, ShouldDisableByStatusCode(404))
require.True(t, ShouldDisableByStatusCode(500))
require.False(t, ShouldDisableByStatusCode(200))
}
28 changes: 28 additions & 0 deletions types/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,20 @@ func (e *NewAPIError) Error() string {
return e.Err.Error()
}

func (e *NewAPIError) ErrorWithStatusCode() string {
if e == nil {
return ""
}
msg := e.Error()
if e.StatusCode == 0 {
return msg
}
if msg == "" {
return fmt.Sprintf("status_code=%d", e.StatusCode)
}
return fmt.Sprintf("status_code=%d, %s", e.StatusCode, msg)
}

func (e *NewAPIError) MaskSensitiveError() string {
if e == nil {
return ""
Expand All @@ -144,6 +158,20 @@ func (e *NewAPIError) MaskSensitiveError() string {
return common.MaskSensitiveInfo(errStr)
}

func (e *NewAPIError) MaskSensitiveErrorWithStatusCode() string {
if e == nil {
return ""
}
msg := e.MaskSensitiveError()
if e.StatusCode == 0 {
return msg
}
if msg == "" {
return fmt.Sprintf("status_code=%d", e.StatusCode)
}
return fmt.Sprintf("status_code=%d, %s", e.StatusCode, msg)
}

func (e *NewAPIError) SetMessage(message string) {
e.Err = errors.New(message)
}
Expand Down
1 change: 1 addition & 0 deletions web/src/components/settings/OperationSetting.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const OperationSetting = () => {
AutomaticDisableChannelEnabled: false,
AutomaticEnableChannelEnabled: false,
AutomaticDisableKeywords: '',
AutomaticDisableStatusCodes: '401',
'monitor_setting.auto_test_channel_enabled': false,
'monitor_setting.auto_test_channel_minutes': 10 /* 签到设置 */,
'checkin_setting.enabled': false,
Expand Down
1 change: 1 addition & 0 deletions web/src/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ export * from './token';
export * from './boolean';
export * from './dashboard';
export * from './passkey';
export * from './statusCodeRules';
Loading