mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-16 15:43:13 +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 过滤规则。
94 lines
3.4 KiB
Go
94 lines
3.4 KiB
Go
//go:build unit
|
|
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
// 三条传输层失败处理的共同契约:请求 context 已取消(客户端断开)时不记 Ops 上游错误事件;
|
|
// 请求 context 仍存活时即便错误是 context.Canceled 也照常记录,不归为客户端断开。
|
|
func TestTransportErrorHandlers_ClientDisconnectRecordsNoOpsEvent(t *testing.T) {
|
|
account := &Account{ID: 9, Name: "acc", Platform: PlatformAnthropic}
|
|
clientErr := &url.Error{Op: "Post", URL: "https://upstream.example/v1/messages", Err: context.Canceled}
|
|
canceledCtx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
handlers := []struct {
|
|
name string
|
|
handle func(ctx context.Context, c *gin.Context) error
|
|
}{
|
|
{"anthropic", func(ctx context.Context, c *gin.Context) error {
|
|
s := &GatewayService{accountRepo: &transportTempUnschedRepoStub{}}
|
|
return s.handleUpstreamTransportError(ctx, c, account, clientErr, OpsUpstreamErrorEvent{})
|
|
}},
|
|
{"gemini", func(ctx context.Context, c *gin.Context) error {
|
|
s := &GeminiMessagesCompatService{accountRepo: &transportTempUnschedRepoStub{}}
|
|
return s.handleUpstreamTransportError(ctx, c, account, clientErr)
|
|
}},
|
|
{"openai", func(ctx context.Context, c *gin.Context) error {
|
|
s := &OpenAIGatewayService{accountRepo: &openaiTransportAccountRepoStub{}}
|
|
return s.handleOpenAIUpstreamTransportError(ctx, c, account, clientErr, false)
|
|
}},
|
|
}
|
|
|
|
for _, h := range handlers {
|
|
t.Run(h.name+"/client disconnected", func(t *testing.T) {
|
|
c := newTransportErrorTestGin(t)
|
|
|
|
err := h.handle(canceledCtx, c)
|
|
|
|
require.ErrorIs(t, err, context.Canceled)
|
|
_, recorded := c.Get(OpsUpstreamErrorsKey)
|
|
require.False(t, recorded, "客户端断开不是上游故障")
|
|
_, messageSet := c.Get(OpsUpstreamErrorMessageKey)
|
|
require.False(t, messageSet)
|
|
})
|
|
t.Run(h.name+"/request context alive", func(t *testing.T) {
|
|
c := newTransportErrorTestGin(t)
|
|
|
|
err := h.handle(context.Background(), c)
|
|
|
|
require.ErrorIs(t, err, context.Canceled)
|
|
raw, recorded := c.Get(OpsUpstreamErrorsKey)
|
|
require.True(t, recorded)
|
|
require.Len(t, raw.([]*OpsUpstreamErrorEvent), 1)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAntigravityCompatTransportError_ClientDisconnectWrites499(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
rec := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(rec)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil).WithContext(ctx)
|
|
|
|
err := (&AntigravityGatewayService{}).handleAntigravityCompatTransportError(c, context.Canceled)
|
|
|
|
require.Error(t, err)
|
|
require.Equal(t, antigravityStatusClientClosed, rec.Code)
|
|
require.Equal(t, "client_disconnected", gjson.Get(rec.Body.String(), "error.type").String())
|
|
}
|
|
|
|
func TestAntigravityWriteGoogleError_ClientClosedStatus(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
rec := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(rec)
|
|
c.Request = httptest.NewRequest(http.MethodPost, "/v1beta/models/gemini-2.5-flash:generateContent", nil)
|
|
|
|
_ = (&AntigravityGatewayService{}).writeGoogleError(c, antigravityStatusClientClosed, "Client disconnected before upstream response")
|
|
|
|
require.Equal(t, antigravityStatusClientClosed, rec.Code)
|
|
require.Equal(t, "CANCELLED", gjson.Get(rec.Body.String(), "error.status").String())
|
|
}
|