mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 03:52:26 +08:00
Google 格式错误体没有 type 字段,ops 一律记为 api_error,客户端参数错误、 引用不存在的缓存等都被归为平台内部错误。 - googleError 写出时按状态码向 ops 登记错误类型:400/413 invalid_request_error、 401 authentication_error、403 permission_error、404 not_found_error, 429 与 5xx 不登记 - 计费、并发与风控拦截沿用 Claude / OpenAI 格式错误的类型与错误码,归类口径一致 - 存储价未配置、显式缓存未启用改用 googlePlatformError,仍按平台错误统计; 错误透传规则改写的上游错误不登记本地类型 - 上游返回的缓存不存在标记为请求级失败:按客户端请求错误分类, 上游原始错误保留为明细但不参与上游归因
136 lines
5.7 KiB
Go
136 lines
5.7 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// recordGoogleFormatOpsError 让 handle 以 Google 错误格式回写错误,返回 ops 中间件记录的错误日志。
|
|
func recordGoogleFormatOpsError(t *testing.T, handle gin.HandlerFunc) *service.OpsInsertErrorLogInput {
|
|
t.Helper()
|
|
setupOpsErrorLogTestQueue(t, 1)
|
|
gin.SetMode(gin.TestMode)
|
|
ops := service.NewOpsService(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
|
router := gin.New()
|
|
router.Use(OpsErrorLoggerMiddleware(ops))
|
|
router.POST("/v1beta/cachedContents", handle)
|
|
|
|
router.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/v1beta/cachedContents", nil))
|
|
|
|
require.Equal(t, int64(1), OpsErrorLogQueueLength())
|
|
return (<-opsErrorLogQueue).entry
|
|
}
|
|
|
|
func TestOpsErrorLoggerMiddleware_GoogleClientErrorsClassifiedAsClient(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
status int
|
|
errType string
|
|
phase string
|
|
}{
|
|
{status: http.StatusBadRequest, errType: "invalid_request_error", phase: "request"},
|
|
{status: http.StatusRequestEntityTooLarge, errType: "invalid_request_error", phase: "request"},
|
|
{status: http.StatusUnauthorized, errType: "authentication_error", phase: "auth"},
|
|
{status: http.StatusForbidden, errType: "permission_error", phase: "request"},
|
|
{status: http.StatusNotFound, errType: "not_found_error", phase: "request"},
|
|
} {
|
|
t.Run(http.StatusText(tc.status), func(t *testing.T) {
|
|
entry := recordGoogleFormatOpsError(t, func(c *gin.Context) {
|
|
googleError(c, tc.status, "client request problem")
|
|
})
|
|
require.Equal(t, tc.status, entry.StatusCode)
|
|
require.Equal(t, tc.errType, entry.ErrorType)
|
|
require.Equal(t, tc.phase, entry.ErrorPhase)
|
|
require.Equal(t, "client", entry.ErrorOwner)
|
|
require.Equal(t, "client_request", entry.ErrorSource)
|
|
require.Equal(t, "P3", entry.Severity)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOpsErrorLoggerMiddleware_GooglePlatformErrorStaysPlatform(t *testing.T) {
|
|
entry := recordGoogleFormatOpsError(t, func(c *gin.Context) {
|
|
googlePlatformError(c, http.StatusBadRequest, "Explicit context caching is not available for model m: storage pricing is not configured")
|
|
})
|
|
require.Equal(t, "api_error", entry.ErrorType)
|
|
require.Equal(t, "internal", entry.ErrorPhase)
|
|
require.Equal(t, "platform", entry.ErrorOwner)
|
|
require.Equal(t, "gateway", entry.ErrorSource)
|
|
}
|
|
|
|
func TestOpsErrorLoggerMiddleware_GoogleServerErrorsNotTypedByStatus(t *testing.T) {
|
|
entry := recordGoogleFormatOpsError(t, func(c *gin.Context) {
|
|
googleError(c, http.StatusInternalServerError, "Failed to persist cached content")
|
|
})
|
|
require.Equal(t, "api_error", entry.ErrorType)
|
|
require.Equal(t, "platform", entry.ErrorOwner)
|
|
}
|
|
|
|
func TestOpsErrorLoggerMiddleware_GoogleBillingErrorMatchesClaudeFormat(t *testing.T) {
|
|
entry := recordGoogleFormatOpsError(t, func(c *gin.Context) {
|
|
status, code, message, _ := billingErrorDetails(service.ErrInsufficientBalance)
|
|
googleErrorWithType(c, status, code, "", message)
|
|
})
|
|
require.Equal(t, "billing_error", entry.ErrorType)
|
|
require.Equal(t, "request", entry.ErrorPhase)
|
|
require.Equal(t, "client", entry.ErrorOwner)
|
|
require.True(t, entry.IsBusinessLimited)
|
|
}
|
|
|
|
func TestOpsErrorLoggerMiddleware_GoogleConcurrencyErrorClassifiedAsRequest(t *testing.T) {
|
|
entry := recordGoogleFormatOpsError(t, func(c *gin.Context) {
|
|
googleConcurrencyError(c, &ConcurrencyError{SlotType: "user"}, "user")
|
|
})
|
|
require.Equal(t, http.StatusTooManyRequests, entry.StatusCode)
|
|
require.Equal(t, "rate_limit_error", entry.ErrorType)
|
|
require.Equal(t, "request", entry.ErrorPhase)
|
|
require.Equal(t, "client", entry.ErrorOwner)
|
|
}
|
|
|
|
func TestOpsErrorLoggerMiddleware_GoogleErrorAfterUpstreamFailureKeepsUpstreamAttribution(t *testing.T) {
|
|
entry := recordGoogleFormatOpsError(t, func(c *gin.Context) {
|
|
service.SetOpsUpstreamError(c, http.StatusBadRequest, "upstream rejected", "")
|
|
googleError(c, http.StatusBadRequest, "upstream rejected")
|
|
})
|
|
require.Equal(t, "upstream", entry.ErrorPhase)
|
|
require.Equal(t, "provider", entry.ErrorOwner)
|
|
require.Equal(t, "upstream_http", entry.ErrorSource)
|
|
}
|
|
|
|
func TestOpsErrorLoggerMiddleware_RequestScopedUpstreamErrorClassifiedAsClient(t *testing.T) {
|
|
entry := recordGoogleFormatOpsError(t, func(c *gin.Context) {
|
|
service.SetOpsUpstreamError(c, http.StatusBadRequest, "Invalid resource state for cache content 8079767076522164224.", "")
|
|
c.Set(service.OpsUpstreamErrorsKey, []*service.OpsUpstreamErrorEvent{{
|
|
Platform: service.PlatformGemini, AccountID: 173, UpstreamStatusCode: http.StatusBadRequest, Kind: "http_error",
|
|
Message: "Invalid resource state for cache content 8079767076522164224.",
|
|
}})
|
|
service.MarkOpsRequestScopedError(c, "permission_error")
|
|
c.Data(http.StatusForbidden, "application/json", []byte(service.GeminiCachedContentNotFoundResponse))
|
|
})
|
|
require.Equal(t, http.StatusForbidden, entry.StatusCode)
|
|
require.Equal(t, "permission_error", entry.ErrorType)
|
|
require.Equal(t, "request", entry.ErrorPhase)
|
|
require.Equal(t, "client", entry.ErrorOwner)
|
|
require.Equal(t, "client_request", entry.ErrorSource)
|
|
require.False(t, entry.IsBusinessLimited)
|
|
require.NotNil(t, entry.UpstreamStatusCode, "上游原始错误保留为明细")
|
|
require.Equal(t, http.StatusBadRequest, *entry.UpstreamStatusCode)
|
|
}
|
|
|
|
func TestSetOpsLocalErrorTypeClearsRequestScopedMark(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|
service.MarkOpsRequestScopedError(c, "permission_error")
|
|
require.True(t, service.IsOpsRequestScopedError(c))
|
|
|
|
service.SetOpsLocalErrorType(c, "", "")
|
|
require.False(t, service.IsOpsRequestScopedError(c))
|
|
errType, code := service.OpsLocalErrorType(c)
|
|
require.Empty(t, errType)
|
|
require.Empty(t, code)
|
|
}
|