fix(errors): 区分平台限流并保留上游状态码
原因:公开错误层将平台并发限流误标为上游限流,并把多种上游 4xx 统一压成 400,影响定位和客户端处理。 影响:新增公开错误 source,平台限流使用 gateway_rate_limited,上游请求按安全分类返回对应状态;数据库与管理端继续保留原始错误码、消息和状态用于审计。 验证:Go 全量测试、pnpm test、pnpm lint、pnpm build、pnpm openapi、gofmt 和 diff 检查均通过。
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/clients"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/publicerror"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
)
|
||||
|
||||
func TestPublicHTTPErrorReportsSourceAndPreservesSafeUpstreamStatus(t *testing.T) {
|
||||
upstream := httptest.NewRecorder()
|
||||
writeProtocolError(upstream, clients.ProtocolOpenAIImages, http.StatusNotFound, "404 page not found at private upstream route", nil, "http_404")
|
||||
if upstream.Code != http.StatusNotFound {
|
||||
t.Fatalf("upstream status = %d, want 404; body=%s", upstream.Code, upstream.Body.String())
|
||||
}
|
||||
var upstreamBody map[string]any
|
||||
if err := json.Unmarshal(upstream.Body.Bytes(), &upstreamBody); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
upstreamError := requireObject(t, upstreamBody["error"])
|
||||
upstreamDetails := requireObject(t, upstreamError["details"])
|
||||
upstreamPublic := requireObject(t, upstreamDetails["publicError"])
|
||||
if upstreamError["code"] != "upstream_not_found" || upstreamPublic["source"] != "upstream" {
|
||||
t.Fatalf("unexpected upstream response: %+v", upstreamBody)
|
||||
}
|
||||
if strings.Contains(upstream.Body.String(), "private upstream route") {
|
||||
t.Fatalf("upstream response leaked raw message: %s", upstream.Body.String())
|
||||
}
|
||||
|
||||
gateway := httptest.NewRecorder()
|
||||
writeError(gateway, http.StatusTooManyRequests, "concurrency limit is saturated and queueing is disabled", "gateway_rate_limited")
|
||||
if gateway.Code != http.StatusTooManyRequests {
|
||||
t.Fatalf("gateway status = %d, want 429; body=%s", gateway.Code, gateway.Body.String())
|
||||
}
|
||||
var gatewayBody map[string]any
|
||||
if err := json.Unmarshal(gateway.Body.Bytes(), &gatewayBody); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gatewayError := requireObject(t, gatewayBody["error"])
|
||||
if gatewayError["code"] != "gateway_rate_limited" || gatewayError["source"] != "gateway" {
|
||||
t.Fatalf("unexpected gateway response: %+v", gatewayBody)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicGatewayTaskSanitizesCopyAndKeepsRawAuditFields(t *testing.T) {
|
||||
rawMessage := `404 page not found: {"privateProject":"secret"}`
|
||||
task := store.GatewayTask{
|
||||
ID: "task-1",
|
||||
Status: "failed",
|
||||
ErrorCode: "http_404",
|
||||
ErrorMessage: rawMessage,
|
||||
Attempts: []store.TaskAttempt{{
|
||||
AttemptNo: 1,
|
||||
Status: "failed",
|
||||
StatusCode: http.StatusNotFound,
|
||||
ErrorCode: "http_404",
|
||||
ErrorMessage: rawMessage,
|
||||
}},
|
||||
}
|
||||
|
||||
public := publicGatewayTask(task)
|
||||
if public.ErrorCode != "upstream_not_found" || public.ErrorMessage == rawMessage || public.PublicError == nil || public.PublicError.Source != "upstream" {
|
||||
t.Fatalf("unexpected public task error: %+v", public)
|
||||
}
|
||||
if strings.Contains(public.ErrorMessage, "secret") || public.Attempts[0].ErrorMessage == rawMessage {
|
||||
t.Fatalf("public task leaked upstream details: %+v", public)
|
||||
}
|
||||
if task.ErrorCode != "http_404" || task.ErrorMessage != rawMessage || task.Attempts[0].ErrorCode != "http_404" || task.Attempts[0].ErrorMessage != rawMessage {
|
||||
t.Fatalf("public conversion mutated raw audit fields: %+v", task)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskErrorHTTPStatusRebuildsLegacySnapshotFromAttempt(t *testing.T) {
|
||||
legacy := publicerror.Error{
|
||||
Code: "upstream_request_rejected",
|
||||
HTTPStatus: http.StatusBadRequest,
|
||||
Version: "v1",
|
||||
}
|
||||
task := store.GatewayTask{
|
||||
ErrorCode: "http_422",
|
||||
PublicError: &legacy,
|
||||
Attempts: []store.TaskAttempt{{StatusCode: http.StatusUnprocessableEntity}},
|
||||
}
|
||||
if got := taskErrorHTTPStatus(task); got != http.StatusUnprocessableEntity {
|
||||
t.Fatalf("taskErrorHTTPStatus = %d, want %d", got, http.StatusUnprocessableEntity)
|
||||
}
|
||||
public := publicTaskError(task)
|
||||
if public.Code != "upstream_unprocessable_request" || public.HTTPStatus != http.StatusUnprocessableEntity || public.Source != "upstream" {
|
||||
t.Fatalf("legacy public snapshot was not rebuilt: %+v", public)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user