// Copyright 2026 Gin Core Team. All rights reserved. // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. package gin import ( "net/http" "github.com/gin-gonic/gin/codec/json" ) // Problem represents an RFC 9457 problem details object. // See https://www.rfc-editor.org/rfc/rfc9457 for the meaning of each member. type Problem struct { // Type is a URI reference that identifies the problem type. // When empty it is omitted from the output, which consumers // must interpret as "about:blank" (RFC 9457 section 3.1.1). Type string `json:"type,omitempty"` // Title is a short, human-readable summary of the problem type. Title string `json:"title,omitempty"` // Status is the HTTP status code generated by the origin server. Status int `json:"status,omitempty"` // Detail is a human-readable explanation specific to this occurrence. Detail string `json:"detail,omitempty"` // Instance is a URI reference that identifies the specific occurrence. Instance string `json:"instance,omitempty"` // Extensions holds problem type specific extension members // (RFC 9457 section 3.2). They are serialized as top-level members // of the problem details object. Extension keys that collide with // a standard member name are ignored. Extensions map[string]any `json:"-"` } // MarshalJSON implements the json.Marshaler interface, serializing // Extensions as top-level members of the problem details object. func (p Problem) MarshalJSON() ([]byte, error) { type problem Problem // avoid infinite recursion if len(p.Extensions) == 0 { return json.API.Marshal(problem(p)) } obj := make(map[string]any, len(p.Extensions)+5) for k, v := range p.Extensions { obj[k] = v } if p.Type != "" { obj["type"] = p.Type } if p.Title != "" { obj["title"] = p.Title } if p.Status != 0 { obj["status"] = p.Status } if p.Detail != "" { obj["detail"] = p.Detail } if p.Instance != "" { obj["instance"] = p.Instance } return json.API.Marshal(obj) } // ProblemDetails returns a middleware that renders errors attached to the // Context (see Context.Error) as an RFC 9457 problem details JSON response. // It does nothing if no errors were attached or if a response body or header // was already written. The response status code is kept when it is an error // status, otherwise it defaults to 500 Internal Server Error. // WARNING: the last attached error's message is exposed to the client as the // problem detail member. Do not attach errors whose messages must stay // private, or write the response yourself instead. func ProblemDetails() HandlerFunc { return func(c *Context) { c.Next() if len(c.Errors) == 0 || c.Writer.Written() { return } status := c.Writer.Status() if status < http.StatusBadRequest { status = http.StatusInternalServerError } c.ProblemJSON(status, Problem{Detail: c.Errors.Last().Error()}) } }