gin/problem.go
Miguel Quintero e08d632300 feat(gin): add ProblemDetails middleware rendering Context errors as RFC 9457
ProblemDetails() renders errors attached via Context.Error as an RFC 9457
problem details JSON response when the handler chain finishes without
writing a response. The response status code is kept when it is an error
status, otherwise it defaults to 500 Internal Server Error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 16:43:58 -04:00

86 lines
2.9 KiB
Go

// 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()})
}
}