125 lines
4.0 KiB
Go
125 lines
4.0 KiB
Go
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)
|
||
|
|
}
|