package problem import ( "encoding/json" "net/http" "strconv" ) // Problem represents an RFC 7807 / RFC 9457 problem document. // We use 9457 as the canonical reference (HTTP Semantics updates). type Problem struct { Type string `json:"type"` // stable URI per rejection reason Title string `json:"title"` // human-readable summary Status int `json:"status"` // HTTP status code Detail string `json:"detail"` // human-useful detail, names offending input Instance string `json:"instance,omitempty"` // URI of the affected resource RetryAfter *int `json:"retry_after,omitempty"` // seconds until retry is safe Extra map[string]interface{} `json:"extra,omitempty"` // additional fields } // NewProblem creates a new problem document with the given parameters. func NewProblem(statusCode int, typeURI, title, detail string) *Problem { return &Problem{ Type: typeURI, Title: title, Status: statusCode, Detail: detail, Extra: make(map[string]interface{}), } } // WithRetryAfter sets the Retry-After field (in seconds). func (p *Problem) WithRetryAfter(seconds int) *Problem { p.RetryAfter = &seconds return p } // WithInstance sets the Instance field. func (p *Problem) WithInstance(instance string) *Problem { p.Instance = instance return p } // WithExtra adds extra fields to the problem document. func (p *Problem) WithExtra(key string, value interface{}) *Problem { p.Extra[key] = value return p } // Write sends the problem document to the HTTP response writer. func (p *Problem) Write(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/problem+json") // Set Retry-After header if present if p.RetryAfter != nil { w.Header().Set("Retry-After", strconv.Itoa(*p.RetryAfter)) } w.WriteHeader(p.Status) body, err := json.Marshal(p) if err != nil { return err } _, err = w.Write(body) return err } // Common problem types const ( TypeBadRequest = "about:blank#bad-request" TypeUnauthorized = "about:blank#unauthorized" TypeForbidden = "about:blank#forbidden" TypeNotFound = "about:blank#not-found" TypeMethodNotAllowed = "about:blank#method-not-allowed" TypeConflict = "about:blank#conflict" TypeGone = "about:blank#gone" TypePayloadTooLarge = "about:blank#payload-too-large" TypeUnprocessable = "about:blank#unprocessable-entity" TypeTooManyRequests = "about:blank#too-many-requests" TypeInternalError = "about:blank#internal-server-error" TypeNotImplemented = "about:blank#not-implemented" TypeUnavailable = "about:blank#service-unavailable" ) // Common constructors func BadRequest(detail string) *Problem { return NewProblem(http.StatusBadRequest, TypeBadRequest, "Bad Request", detail) } func Unauthorized(detail string) *Problem { return NewProblem(http.StatusUnauthorized, TypeUnauthorized, "Unauthorized", detail) } func Forbidden(detail string) *Problem { return NewProblem(http.StatusForbidden, TypeForbidden, "Forbidden", detail) } func NotFound(detail string) *Problem { return NewProblem(http.StatusNotFound, TypeNotFound, "Not Found", detail) } func PayloadTooLarge(detail string) *Problem { return NewProblem(http.StatusRequestEntityTooLarge, TypePayloadTooLarge, "Payload Too Large", detail) } func UnprocessableEntity(detail string) *Problem { return NewProblem(http.StatusUnprocessableEntity, TypeUnprocessable, "Unprocessable Entity", detail) } func TooManyRequests(detail string, retryAfter int) *Problem { return NewProblem(http.StatusTooManyRequests, TypeTooManyRequests, "Too Many Requests", detail). WithRetryAfter(retryAfter) } func InternalServerError(detail string) *Problem { return NewProblem(http.StatusInternalServerError, TypeInternalError, "Internal Server Error", detail) } func ServiceUnavailable(detail string, retryAfter int) *Problem { return NewProblem(http.StatusServiceUnavailable, TypeUnavailable, "Service Unavailable", detail). WithRetryAfter(retryAfter) }