fix(acceptance): 阻断本地控制面漂移污染验收
本地 K3s 节点此前在 Kubelet 中登记为宿主机资源,负载可挤压 etcd 并在 server 重启后继续污染同一 Run。现在为三节点设置真实可调度预算、独立 etcd/数据卷和锁定依赖镜像,并持续核对 server 与 API/etcd 健康。\n\n每次验收生成独立 Run ID,报告使用独占或原子写入,负载错误记录具体阶段;数据库鉴权不可用返回带 Retry-After 的 503,避免基础设施故障被误报为 401。\n\n验证:Go 全量测试、go vet、gofmt、OpenAPI、bash -n、ShellCheck、报告测试和 k3d 配置解析均通过。
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
|
||||
@@ -19,6 +20,9 @@ func (s *Server) requireProtocolUser(protocol string, next http.Handler) http.Ha
|
||||
var requestErr *auth.RequestAuthError
|
||||
if errors.As(err, &requestErr) {
|
||||
status, code, message = requestErr.Status, requestErr.Code, requestErr.Message
|
||||
if requestErr.RetryAfterSeconds > 0 {
|
||||
w.Header().Set("Retry-After", strconv.Itoa(requestErr.RetryAfterSeconds))
|
||||
}
|
||||
}
|
||||
writeProtocolError(w, protocol, status, message, nil, code)
|
||||
return
|
||||
|
||||
@@ -3,6 +3,7 @@ package httpapi
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -13,6 +14,39 @@ import (
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
)
|
||||
|
||||
func TestProtocolAPIKeyStoreFailureIs503InsteadOf401(t *testing.T) {
|
||||
authenticator := auth.New("test-secret", "", "")
|
||||
authenticator.LocalAPIKeyVerifier = func(context.Context, string) (*auth.User, error) {
|
||||
return nil, errors.New("database unavailable")
|
||||
}
|
||||
server := &Server{auth: authenticator}
|
||||
handler := server.requireProtocolUser(clients.ProtocolGeminiGenerateContent, http.HandlerFunc(
|
||||
func(http.ResponseWriter, *http.Request) {
|
||||
t.Fatal("unavailable authentication store must not call the protocol handler")
|
||||
},
|
||||
))
|
||||
request := httptest.NewRequest(http.MethodPost, "/v1beta/models/test:generateContent", nil)
|
||||
request.Header.Set("Authorization", "Bearer sk-gw-local")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("status=%d, want 503; body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if recorder.Header().Get("Retry-After") != "2" {
|
||||
t.Fatalf("Retry-After=%q, want 2", recorder.Header().Get("Retry-After"))
|
||||
}
|
||||
var body map[string]any
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
errorBody := requireObject(t, body["error"])
|
||||
if errorBody["code"] != float64(http.StatusServiceUnavailable) || errorBody["status"] != "UNAVAILABLE" {
|
||||
t.Fatalf("unexpected Gemini unavailable response: %+v", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocolErrorsUseOfficialShapesWithoutGatewayExtensions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -106,6 +106,7 @@ func geminiGenerateContentModelFromPath(prefix string, requestPath string) (stri
|
||||
// @Failure 401 {object} GeminiErrorEnvelope
|
||||
// @Failure 403 {object} GeminiErrorEnvelope
|
||||
// @Failure 502 {object} GeminiErrorEnvelope
|
||||
// @Failure 503 {object} GeminiErrorEnvelope
|
||||
// @Router /api/v1/models/{model}:generateContent [post]
|
||||
// @Router /api/v1/models/{model}:streamGenerateContent [post]
|
||||
func (s *Server) geminiGenerateContent(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -1420,6 +1420,7 @@ func openAIImagesDoc() {}
|
||||
// @Failure 429 {object} ErrorEnvelope
|
||||
// @Failure 504 {object} ErrorEnvelope
|
||||
// @Failure 502 {object} ErrorEnvelope
|
||||
// @Failure 503 {object} ErrorEnvelope
|
||||
// @Router /api/v1/videos/generations [post]
|
||||
// @Router /api/v1/song/generations [post]
|
||||
// @Router /api/v1/music/generations [post]
|
||||
|
||||
@@ -28,6 +28,7 @@ const volcesContentsCompatibilityMarker = "volces_contents_generations_v3"
|
||||
// @Failure 401 {object} VolcesErrorEnvelope
|
||||
// @Failure 429 {object} VolcesErrorEnvelope
|
||||
// @Failure 502 {object} VolcesErrorEnvelope
|
||||
// @Failure 503 {object} VolcesErrorEnvelope
|
||||
// @Router /api/v1/contents/generations/tasks [post]
|
||||
func (s *Server) createVolcesContentsGenerationTask(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := auth.UserFromContext(r.Context())
|
||||
@@ -186,6 +187,7 @@ func (s *Server) createLegacyVolcesVideoGeneration(w http.ResponseWriter, r *htt
|
||||
// @Success 200 {object} EasyAIGeneratedResponse
|
||||
// @Failure 404 {object} EasyAIGeneratedResponse
|
||||
// @Failure 410 {object} EasyAIGeneratedResponse
|
||||
// @Failure 503 {object} ErrorEnvelope
|
||||
// @Router /api/v1/ai/result/{taskID} [get]
|
||||
func (s *Server) getEasyAITaskResult(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := auth.UserFromContext(r.Context())
|
||||
|
||||
Reference in New Issue
Block a user