diff --git a/spinifex/gateway/auth.go b/spinifex/gateway/auth.go index fabb06922..e46027614 100644 --- a/spinifex/gateway/auth.go +++ b/spinifex/gateway/auth.go @@ -85,6 +85,11 @@ func (gw *GatewayConfig) SigV4AuthMiddleware() func(http.Handler) http.Handler { // every rejection below still shares. auditFrom(r.Context()).setAccessKeyID(sig.Credential.AccessKeyID) + // Carry the signed service now, so a rejection below (a signature + // mismatch on a bedrock Converse) renders in the client's format — + // GetService reads this to pick JSON over XML for the bedrock family. + r = r.WithContext(context.WithValue(r.Context(), ctxService, sig.Credential.Service)) + // Reject unknown services before crypto; otherwise Verify re-signs with // the client-claimed service name and rubber-stamps the scope. if !supportedServices[sig.Credential.Service] { @@ -362,7 +367,7 @@ func (gw *GatewayConfig) resolveSessionAKID(r *http.Request, accessKeyID, client }, "" } -// writeSigV4Error writes an EC2-compatible XML error response for auth failures. +// writeSigV4Error writes an auth-failure error in the service-appropriate format. // Every auth rejection funnels through here, a rate-limit lockout included, so // this is the one place that has to record the verdict onto the request. func (gw *GatewayConfig) writeSigV4Error(w http.ResponseWriter, r *http.Request, errorCode string) { @@ -374,6 +379,15 @@ func (gw *GatewayConfig) writeSigV4Error(w http.ResponseWriter, r *http.Request, errorMsg = awserrors.ErrorMessage{HTTPCode: 500, Message: "Internal error"} } + // AWS JSON 1.1 services (bedrock family, EKS, …) need a JSON error body, or + // the SDK chokes deserializing our XML into its shape. + if svc, _ := gw.GetService(r); jsonErrorService(svc) { + w.Header().Set("Content-Type", eksJSONContentType) + w.WriteHeader(errorMsg.HTTPCode) + _, _ = w.Write(GenerateEKSErrorResponse(errorCode, errorMsg.Message, requestID)) + return + } + xmlError := GenerateEC2ErrorResponse(errorCode, errorMsg.Message, requestID) w.Header().Set("Content-Type", "application/xml") diff --git a/spinifex/gateway/gateway.go b/spinifex/gateway/gateway.go index 60a449a4c..ff5363d6c 100644 --- a/spinifex/gateway/gateway.go +++ b/spinifex/gateway/gateway.go @@ -331,6 +331,18 @@ func (gw *GatewayConfig) throttleKeyFuncs() []ratelimit.KeyFunc { // eksJSONContentType is the AWS REST-JSON 1.1 content type EKS clients expect. const eksJSONContentType = "application/x-amz-json-1.1" +// jsonErrorService reports whether svc returns AWS JSON 1.1 errors rather than +// XML. One source of truth so every error emitter agrees with ErrorHandler; an +// XML body to these clients is an unparseable "") } +func TestWriteThrottleError_Bedrock(t *testing.T) { + gw := &GatewayConfig{DisableLogging: true} + + req := httptest.NewRequest(http.MethodPost, "/model/meta.llama3-2-1b-instruct-v1%3A0/converse", nil) + req = req.WithContext(context.WithValue(req.Context(), ctxService, "bedrock")) + w := httptest.NewRecorder() + + gw.writeThrottleError(w, req) + + resp := w.Result() + body, _ := io.ReadAll(resp.Body) + assert.Equal(t, eksJSONContentType, resp.Header.Get("Content-Type"), "bedrock must get JSON, not XML") + assert.Contains(t, string(body), `"__type"`) + assert.NotContains(t, string(body), "