package runtime import ( "encoding/json" "net/http" "github.com/tegwick/fluid-core/internal/contract" ) // ErrorKind classifies a response the interface produced rather than the // backend. These map onto the telemetry error classes so that a response and // the evidence it generates cannot disagree. type ErrorKind string const ( ErrorValidation ErrorKind = "validation" ErrorUnknownPath ErrorKind = "unknown_path" ErrorUnsupported ErrorKind = "unsupported_parameter" ErrorAuthorization ErrorKind = "authorization" ErrorUnavailable ErrorKind = "backend_failure" ErrorTimeout ErrorKind = "timeout" ErrorPolicy ErrorKind = "policy_rejection" ErrorNoCapability ErrorKind = "missing_capability" ) // ErrorBody is the interface's error representation. // // ArchitectureBlueprint.md section 5.7: errors should carry enough structured // information to improve observability without leaking backend detail, and may // include a correlation reference that lets downstream analysis link the // response a consumer saw to the pressure it generated. type ErrorBody struct { Kind ErrorKind `json:"kind"` // Message is written for the consumer and must stay free of backend // internals: hostnames, stack traces, driver errors, upstream payloads. Message string `json:"message"` // Correlation is the reference a consumer can quote back, and the key that // ties this response to its telemetry. Correlation string `json:"correlation"` // Revision tells the consumer which contract answered. Without it a client // debugging an unexpected response has no way to know what it was talking to. Revision contract.RevisionID `json:"revision,omitempty"` // Field names the offending input for validation failures. Field string `json:"field,omitempty"` // Feedback points at the explicit-feedback endpoint. Turning a dead end into // an invitation is the cheapest pressure signal the interface can collect. Feedback string `json:"feedback,omitempty"` } // statusFor maps an error kind to its HTTP status. var statusFor = map[ErrorKind]int{ ErrorValidation: http.StatusBadRequest, ErrorUnknownPath: http.StatusNotFound, ErrorUnsupported: http.StatusBadRequest, ErrorAuthorization: http.StatusForbidden, ErrorUnavailable: http.StatusBadGateway, ErrorTimeout: http.StatusGatewayTimeout, ErrorPolicy: http.StatusForbidden, ErrorNoCapability: http.StatusNotImplemented, } // ResponsePolicy renders interface errors consistently. type ResponsePolicy struct { // FeedbackPath, when set, is advertised on every error. FeedbackPath string } // WriteError renders an error response. func (p ResponsePolicy) WriteError(w http.ResponseWriter, kind ErrorKind, correlation, message string, rev contract.RevisionID, field string) { status, ok := statusFor[kind] if !ok { status = http.StatusInternalServerError } body := ErrorBody{ Kind: kind, Message: message, Correlation: correlation, Revision: rev, Field: field, Feedback: p.FeedbackPath, } w.Header().Set("Content-Type", "application/json") w.Header().Set("X-FLUID-Correlation", correlation) if rev != "" { w.Header().Set("X-FLUID-Revision", string(rev)) } w.WriteHeader(status) _ = json.NewEncoder(w).Encode(body) }