package api import ( "net/http" "reflect" "sync" "github.com/danielgtaylor/huma/v2" ) // ProblemDetail is one entry of a problem's errors list (a failed validation, say). type ProblemDetail struct { Message string `json:"message" doc:"Error message text."` Location string `json:"location,omitempty" doc:"Where the error occurred, e.g. query.per_page or body.parents."` Value any `json:"value,omitempty" doc:"The offending value."` } // Problem is the RFC 9457 problem details body of every error response. It // replaces huma's ErrorModel only to make title and status required, which // the generated TypeScript types rely on. type Problem struct { Type string `json:"type,omitempty" doc:"URI reference identifying the problem type; about:blank when absent."` Title string `json:"title" example:"Not Found"` Status int `json:"status" example:"404" doc:"HTTP status code."` Detail string `json:"detail,omitempty" doc:"Explanation specific to this occurrence."` Instance string `json:"instance,omitempty"` Errors []ProblemDetail `json:"errors,omitempty" doc:"Individual errors, e.g. one per invalid parameter."` } // Error returns the detail, falling back to the title. func (p *Problem) Error() string { if p.Detail != "" { return p.Detail } return p.Title } // GetStatus is the HTTP status of the response. func (p *Problem) GetStatus() int { return p.Status } // ContentType makes huma answer application/problem+json. func (p *Problem) ContentType(ct string) string { if ct == "application/json" { return "application/problem+json" } return ct } // newProblem is huma.NewError for this API. func newProblem(status int, msg string, errs ...error) huma.StatusError { p := &Problem{Title: http.StatusText(status), Status: status, Detail: msg} for _, e := range errs { if d, ok := e.(huma.ErrorDetailer); ok { x := d.ErrorDetail() p.Errors = append(p.Errors, ProblemDetail{Message: x.Message, Location: x.Location, Value: x.Value}) continue } p.Errors = append(p.Errors, ProblemDetail{Message: e.Error()}) } return p } var humaOnce sync.Once // configureHuma sets huma's package-level switches once: arrays are never // nullable, and errors are Problems. func configureHuma() { humaOnce.Do(func() { huma.DefaultArrayNullable = false huma.NewError = newProblem }) } // addDefaultProblem documents the RFC 9457 default response on every // operation, the way the UI contract does, whatever status codes it also lists. func addDefaultProblem(oapi *huma.OpenAPI, op *huma.Operation) { if _, ok := op.Responses["default"]; ok { return } schema := oapi.Components.Schemas.Schema(reflect.TypeFor[Problem](), true, "") op.Responses["default"] = &huma.Response{ Description: "RFC 9457 problem details", Content: map[string]*huma.MediaType{"application/problem+json": {Schema: schema}}, } }