mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
客户端在响应提交前断开时,只有排队/换号阶段和 OpenAI 的兜底写入会标 499。 Claude 非流式与 Gemini 的 Chat Completions / Messages / Responses 兼容入口 在等待上游时断开,由 GatewayHandler.ensureForwardErrorResponse 补写 502; Gemini 原生入口什么都不写,落成 200 并被 ops 记成一条 provider 的 "Recovered upstream error";Antigravity 回写 502。这些行都按上游失败计入 SLA 与上游错误数。 - GatewayHandler.ensureForwardErrorResponse 补客户端断开检查,与 OpenAI 版一致 - GatewayHandler 的 Messages / ChatCompletions / Responses 与 Gemini 原生入口: 首次选号失败时先判断客户端断开;ChatCompletions / Responses 循环开头改用 failoverClientGone,不再直接 return 落成 200 - Gemini 原生入口:forward 失败时补 499 标记;并发槽错误改走 concurrencyErrorResponse 统一映射(断开 499、排队满/超时 429、其余 503) - Antigravity 上游响应前断开回写 499;googleapi 与 Antigravity 的 Google 错误 格式把 499 映射为 CANCELLED - OpenAI alpha search 非 failover 错误分支补断开检查 - Claude / Gemini / OpenAI 传输层错误:请求 context 已取消时不记 ops 上游事件 - ops:IgnoreContextCanceled 开启时跳过没有上游错误上下文的 499;带上游错误的 499(上游失败后客户端没等到换号结果就离开)仍按上游失败落库 计费不变:Gemini 流式中途断开的用量处理不在本次范围。新增 handler/service 单测覆盖各入口的断开场景与 ops 过滤规则。
141 lines
4.0 KiB
Go
141 lines
4.0 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestConcurrencyErrorResponse(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
slotType string
|
|
wantStatus int
|
|
wantType string
|
|
wantCode string
|
|
wantMessage string
|
|
}{
|
|
{
|
|
name: "true concurrency timeout remains rate limit",
|
|
err: &ConcurrencyError{SlotType: "account", IsTimeout: true},
|
|
slotType: "user",
|
|
wantStatus: http.StatusTooManyRequests,
|
|
wantType: "rate_limit_error",
|
|
wantCode: gatewayConcurrencyLimitCode,
|
|
wantMessage: "Concurrency limit exceeded for account, please retry later",
|
|
},
|
|
{
|
|
name: "full local wait queue has gateway code",
|
|
err: &WaitQueueFullError{SlotType: "account"},
|
|
slotType: "account",
|
|
wantStatus: http.StatusTooManyRequests,
|
|
wantType: "rate_limit_error",
|
|
wantCode: gatewayQueueFullCode,
|
|
wantMessage: "Too many pending requests, please retry later",
|
|
},
|
|
{
|
|
name: "client cancellation is not classified as concurrency limit",
|
|
err: context.Canceled,
|
|
slotType: "user",
|
|
wantStatus: statusClientClosedRequest,
|
|
wantType: "api_error",
|
|
wantMessage: "context canceled",
|
|
},
|
|
{
|
|
name: "deadline exceeded is service unavailable",
|
|
err: context.DeadlineExceeded,
|
|
slotType: "user",
|
|
wantStatus: http.StatusServiceUnavailable,
|
|
wantType: "api_error",
|
|
wantMessage: "Service temporarily unavailable, please retry later",
|
|
},
|
|
{
|
|
name: "redis acquire error is service unavailable",
|
|
err: errors.New("redis unavailable"),
|
|
slotType: "user",
|
|
wantStatus: http.StatusServiceUnavailable,
|
|
wantType: "api_error",
|
|
wantMessage: "Service temporarily unavailable, please retry later",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
status, errType, code, message := concurrencyErrorResponse(tt.err, tt.slotType)
|
|
require.Equal(t, tt.wantStatus, status)
|
|
require.Equal(t, tt.wantType, errType)
|
|
require.Equal(t, tt.wantCode, code)
|
|
require.Equal(t, tt.wantMessage, message)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGoogleConcurrencyError(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
wantStatus int
|
|
wantGStatus string
|
|
wantMessage string
|
|
}{
|
|
{
|
|
name: "client cancellation is 499",
|
|
err: context.Canceled,
|
|
wantStatus: statusClientClosedRequest,
|
|
wantGStatus: "CANCELLED",
|
|
wantMessage: "context canceled",
|
|
},
|
|
{
|
|
name: "full wait queue is 429",
|
|
err: &WaitQueueFullError{SlotType: "user"},
|
|
wantStatus: http.StatusTooManyRequests,
|
|
wantGStatus: "RESOURCE_EXHAUSTED",
|
|
wantMessage: "Too many pending requests, please retry later",
|
|
},
|
|
{
|
|
name: "slot wait timeout is 429",
|
|
err: &ConcurrencyError{SlotType: "user", IsTimeout: true},
|
|
wantStatus: http.StatusTooManyRequests,
|
|
wantGStatus: "RESOURCE_EXHAUSTED",
|
|
wantMessage: "Concurrency limit exceeded for user, please retry later",
|
|
},
|
|
{
|
|
name: "acquire backend error is 503",
|
|
err: errors.New("redis unavailable"),
|
|
wantStatus: http.StatusServiceUnavailable,
|
|
wantGStatus: "INTERNAL",
|
|
wantMessage: "Service temporarily unavailable, please retry later",
|
|
},
|
|
}
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(rec)
|
|
|
|
googleConcurrencyError(c, tt.err, "user")
|
|
|
|
require.Equal(t, tt.wantStatus, rec.Code)
|
|
var body struct {
|
|
Error struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
Status string `json:"status"`
|
|
} `json:"error"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &body))
|
|
require.Equal(t, tt.wantStatus, body.Error.Code)
|
|
require.Equal(t, tt.wantGStatus, body.Error.Status)
|
|
require.Equal(t, tt.wantMessage, body.Error.Message)
|
|
})
|
|
}
|
|
}
|