package main import ( "context" "crypto/rand" "encoding/json" "errors" "fmt" "log" "net/http" "os" ) // problemCode is the stable, low-cardinality name a client can key on. The // request ID identifies one occurrence; the code identifies the class of // failure without exposing the wrapped implementation error. type problemCode string const ( problemMethodNotAllowed problemCode = "request.method_not_allowed" problemInvalidRequest problemCode = "request.invalid" problemPayloadTooLarge problemCode = "request.payload_too_large" problemUnauthorized problemCode = "auth.unauthorized" problemStepUpRequired problemCode = "auth.step_up_required" problemResourceNotFound problemCode = "resource.not_found" problemIntegrationOff problemCode = "integration.disabled" problemCoreUnavailable problemCode = "core.unavailable" problemCoreReadFailed problemCode = "core.read_failed" problemCoreWriteFailed problemCode = "core.write_failed" problemCoreChangeFailed problemCode = "core.change_failed" problemToolsChange problemCode = "tools.change_failed" problemRoutinesChange problemCode = "routines.change_failed" problemModelsUnavailable problemCode = "models.unavailable" problemModelsForbidden problemCode = "models.forbidden" problemWebAuthnBegin problemCode = "webauthn.begin_failed" problemWebAuthnFinish problemCode = "webauthn.finish_failed" problemWebAuthnStepUp problemCode = "webauthn.step_up_failed" problemVoiceUnavailable problemCode = "voice.unavailable" problemVoiceTransport problemCode = "voice.transport_failed" problemVoiceResponse problemCode = "voice.response_failed" ) type requestIDKey struct{} // problemLogger is separate from the package-wide logger so the contract test // can capture exactly one problem line without redirecting unrelated output. var problemLogger = log.New(os.Stderr, "", log.LstdFlags) // problemResponse is the one non-success envelope returned by mavweb. Error is // deliberately a public message, never err.Error(). Code is stable across // occurrences; request_id joins this answer to the full server-side log line. type problemResponse struct { Error string `json:"error"` Code problemCode `json:"code"` RequestID string `json:"request_id"` } // withRequestID mints the request identifier at the HTTP boundary. A caller's // X-Request-ID is ignored: accepting it would let an untrusted client forge a // link to another request's logs. The generated ID is also returned on success, // which lets an operator start from any surprising response, not errors alone. func withRequestID(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := rand.Text() w.Header().Set("X-Request-ID", id) ctx := context.WithValue(r.Context(), requestIDKey{}, id) next.ServeHTTP(w, r.WithContext(ctx)) }) } func requestIDFromContext(ctx context.Context) string { id, _ := ctx.Value(requestIDKey{}).(string) return id } func problemRequestID(r *http.Request) string { id := requestIDFromContext(r.Context()) if id == "" { id = rand.Text() } return id } func logProblem(r *http.Request, status int, code problemCode, public string, err error) string { id := problemRequestID(r) if err == nil { err = errors.New(public) } problemLogger.Printf("mavweb problem request_id=%s code=%s status=%d method=%s path=%q: %v", id, code, status, r.Method, r.URL.Path, err) return id } // inlineProblem preserves a useful partial page when one panel fails, while // applying the same disclosure and correlation rules as an HTTP problem. func inlineProblem(r *http.Request, code problemCode, public string, err error) string { id := logProblem(r, http.StatusOK, code, public, err) return fmt.Sprintf("%s (code %s, request %s)", public, code, id) } // writeProblem is the only mavweb HTTP error writer. The wrapped error is // logged in full and only the explicit public message, stable code and request // ID cross the HTTP boundary. func writeProblem(w http.ResponseWriter, r *http.Request, status int, code problemCode, public string, err error) { // Unit-level handlers and embedders may call a handler without installing // the server middleware. They still get the same traceable contract. id := logProblem(r, status, code, public, err) w.Header().Set("X-Request-ID", id) w.Header().Set("Content-Type", "application/problem+json; charset=utf-8") w.Header().Set("Cache-Control", "no-store") w.WriteHeader(status) // A failed client connection leaves nowhere useful to report an encoder // error; the full problem is already in the server log before this write. _ = json.NewEncoder(w).Encode(problemResponse{ Error: public, Code: code, RequestID: id, }) }