Documentation
¶
Overview ¶
Package problem implements RFC 9457 Problem Details for HTTP APIs.
The package centers on Details, an error-compatible value type that can be served directly as an HTTP response. Construct with New and customize an occurrence with Details.Derive; standard members have read-only accessors.
failure := problem.New(func(details *problem.Builder) {
details.Status(http.StatusNotFound)
details.Title("Order not found")
})
_ = failure.Status()
Content negotiation ¶
Problem responses negotiate application/problem+json, application/json, and text/plain in that server order. When no syntactically valid Accept value is supplied, that order applies. For valid headers, the most-specific matching range controls each representation and q=0 excludes it. When every representation is explicitly excluded, Details.ServeHTTP returns 406 without rendering the original problem. If JSON encoding an extension fails, it renders plain text only when text/plain is acceptable; otherwise it returns 406. Malformed quality values invalidate their range, rather than defaulting its quality to one. Media parameters are requirements: text/plain;charset=utf-8 matches the text renderer, while other charsets and unprovided JSON profiles do not. Parameterized matches outrank otherwise equal unparameterized ranges. Responses retain existing Vary tokens and add Accept unless already covered.
Immutability ¶
New and Details.Derive invoke callbacks directly, synchronously, once in argument order on one temporary Builder. Omitted or expanded empty callback slices supply no edits; supplied nil callbacks panic. Both publication and derivation detach outer extension maps and header slices, so a retained builder cannot edit published values. Arbitrary nested extension values remain shared.
Application code should define Details values as package-level variables constructed with New, then derive per-request instances with Details.Derive. Builder.Error retains a cause without serializing it. Details.Extension distinguishes an absent member from a stored nil value; Details.Extensions returns a copy of the outer map.
Defaults and decoding ¶
New resolves zero status to 500, empty type to about:blank, then an empty title from the final status. Details.Derive preserves stored fields without normalization, including inherited titles when status changes. JSON decoding preserves absent status as zero, replaces old state only on success, and clears previous causes and headers on success. Plain encoding omits zero status; HTTP rendering completes defaults and emits the effective status in the body. Stored nonzero statuses are not validated by construction or decoding. Only ServeHTTP replaces statuses outside net/http's 100–999 range with 500; plain JSON, accessors, Defaulted, and identity matching retain the original value.
Matching ¶
errors.Is matches a Details against a template with the same Type, Status and Title; Detail, extension members, headers and the wrapped cause are ignored. See Details.Is.
occurrence := ErrUserNotFound.Derive(func(details *problem.Builder) {
details.Detail("no user 42")
})
errors.Is(occurrence, ErrUserNotFound) // true
Request response context ¶
Middleware can attach a ResponseContext for client-facing RFC 9457 extension members and HTTP response headers. Details.ServeHTTP snapshots it without modifying the original Details value. Explicit Builder.Extension extension members and Details header builders take precedence over matching response-context fields. Renderers always own Content-Type. Snapshot independence applies to outer property maps and header slices, not arbitrary nested property values. Coordinate nested mutation with encoding.
Example
responseContext := problem.NewResponseContext()
responseContext.SetProperty("request_id", "123")
responseContext.AddHeader("Vary", "Accept-Language")
request = request.WithContext(problem.NewContext(request.Context(), responseContext))
ErrNotFound.ServeHTTP(w, request)
Index ¶
- func NewContext(ctx context.Context, responseContext *ResponseContext) context.Context
- type Builder
- func (details *Builder) AddHeader(name, value string) *Builder
- func (details *Builder) Detail(value string) *Builder
- func (details *Builder) Error(err error) *Builder
- func (details *Builder) Extension(name string, value any) *Builder
- func (details *Builder) Header(name string, values ...string) *Builder
- func (details *Builder) Instance(value string) *Builder
- func (details *Builder) RemoveExtension(name string) *Builder
- func (details *Builder) RemoveHeader(name string) *Builder
- func (details *Builder) Status(value int) *Builder
- func (details *Builder) Title(value string) *Builder
- func (details *Builder) Type(value string) *Builder
- type BuilderFunc
- type Details
- func (problem Details) Defaulted(request *http.Request) Details
- func (problem Details) Derive(configure ...BuilderFunc) Details
- func (problem Details) Detail() string
- func (problem Details) Error() string
- func (problem Details) Extension(name string) (any, bool)
- func (problem Details) Extensions() map[string]any
- func (problem Details) HTTPStatus() int
- func (problem Details) Instance() string
- func (problem Details) Is(target error) bool
- func (problem Details) MarshalJSONTo(encoder *jsontext.Encoder) error
- func (problem Details) ServeHTTP(writer http.ResponseWriter, request *http.Request)
- func (problem Details) Status() int
- func (problem Details) Title() string
- func (problem Details) Type() string
- func (problem *Details) UnmarshalJSONFrom(decoder *jsontext.Decoder) error
- func (problem Details) Unwrap() error
- type ResponseContext
- func (responseContext *ResponseContext) AddHeader(name, value string)
- func (responseContext *ResponseContext) DeleteHeader(name string)
- func (responseContext *ResponseContext) DeleteProperty(name string)
- func (responseContext *ResponseContext) Headers() http.Header
- func (responseContext *ResponseContext) Properties() map[string]any
- func (responseContext *ResponseContext) SetHeader(name, value string)
- func (responseContext *ResponseContext) SetProperty(name string, value any)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func NewContext ¶
func NewContext(ctx context.Context, responseContext *ResponseContext) context.Context
NewContext returns a context containing responseContext. A nil responseContext leaves ctx unchanged. The container is retained, not copied; later edits are visible to ServeHTTP when it snapshots this request's context. A nonnil container requires a nonnil parent, as context.WithValue does.
Example:
metadata := problem.NewResponseContext() ctx := problem.NewContext(context.Background(), metadata) attached, found := problem.FromContext(ctx) _, _ = attached, found
Types ¶
type Builder ¶
type Builder struct {
// contains filtered or unexported fields
}
Builder configures one temporary draft. Publication copies outer maps and header slices; nested extension values and causes remain caller-owned.
Example:
failure := problem.New(func(details *problem.Builder) {
details.Status(404).Title("Missing")
})
_ = failure.Title()
func (*Builder) AddHeader ¶
AddHeader appends one value to a canonicalized header and returns the same draft. Existing values retain their order; headers stay out of JSON.
Example:
failure := problem.New(func(details *problem.Builder) {
details.AddHeader("Vary", "Accept")
})
_ = failure
func (*Builder) Detail ¶
Detail replaces the occurrence explanation and returns the same draft. Empty explanations are omitted from JSON.
Example:
failure := problem.New(func(details *problem.Builder) {
details.Detail("Try another name.")
})
_ = failure.Detail()
func (*Builder) Error ¶
Error retains the cause for errors.Is/As traversal and returns the same draft. Nil clears the cause. Causes never become JSON members.
Example:
failure := problem.New(func(details *problem.Builder) {
details.Error(io.EOF)
})
_ = errors.Is(failure, io.EOF)
func (*Builder) Extension ¶
Extension replaces one flattened extension and returns the same draft. Nil is stored, not removed. Values are retained without recursive cloning or encoding; unsupported JSON values fail at encoding time, not during registration.
Example:
failure := problem.New(func(details *problem.Builder) {
details.Extension("order_id", 42)
})
value, found := failure.Extension("order_id")
_, _ = value, found
func (*Builder) Header ¶
Header replaces a canonicalized header with copied values and returns the same draft. Zero values removes it. Rendering owns Content-Type regardless of this setting; headers never become JSON members.
Example:
failure := problem.New(func(details *problem.Builder) {
details.Header("Retry-After", "60")
})
_ = failure
func (*Builder) Instance ¶
Instance replaces the occurrence URI reference and returns the same draft. Empty omits the JSON member until HTTP rendering supplies the request path.
Example:
failure := problem.New(func(details *problem.Builder) {
details.Instance("/orders/42")
})
_ = failure.Instance()
func (*Builder) RemoveExtension ¶
RemoveExtension deletes one extension if present and returns the same draft. Standard members with the same name are unaffected.
Example:
failure := problem.New(func(details *problem.Builder) {
details.RemoveExtension("debug")
})
_, found := failure.Extension("debug")
_ = found // false
func (*Builder) RemoveHeader ¶
RemoveHeader removes all values for a canonicalized header and returns the same draft. Removing an absent header is a no-op.
Example:
failure := problem.New(func(details *problem.Builder) {
details.RemoveHeader("Retry-After")
})
_ = failure
func (*Builder) Status ¶
Status replaces the advisory HTTP status and returns the same draft. New resolves zero to 500; Derive preserves it without changing the title. Values are stored without validation. ServeHTTP renders values outside net/http's three-digit range as 500, without modifying the stored identity.
Example:
failure := problem.New(func(details *problem.Builder) {
details.Status(http.StatusConflict)
})
_ = failure.Status()
func (*Builder) Title ¶
Title replaces the summary and returns the same draft. Empty defaults from the final status only in New; Derive retains the supplied empty value.
Example:
failure := problem.New(func(details *problem.Builder) {
details.Title("Order missing")
})
_ = failure.Title()
type BuilderFunc ¶
type BuilderFunc = func(*Builder)
BuilderFunc edits one temporary problem draft during New or Details.Derive. Callbacks run directly once in order on the same draft; nil entries panic. Publication detaches outer maps and header slices, not arbitrary nested values.
Example:
policy := problem.BuilderFunc(func(details *problem.Builder) {
details.Status(http.StatusNotFound).Title("Order missing")
})
failure := problem.New(policy)
_ = failure.Title()
type Details ¶
type Details struct {
// contains filtered or unexported fields
}
Details is an immutable RFC 9457 problem, an error, and an HTTP handler. Construct with New and customize occurrences with Derive. The zero value is usable for decoding and HTTP rendering, but has unspecified standard members. Only owned outer maps and header slices are detached, not nested extensions.
Example:
failure := problem.New(func(details *problem.Builder) {
details.Status(http.StatusNotFound)
})
_ = failure.Title()
func BadRequest ¶
func BadRequest() Details
BadRequest returns a copy of the 400 Bad Request problem template. Its stored type is about:blank, matching an equivalent New value.
Example:
failure := problem.BadRequest() _ = failure.Status() // 400
func Conflict ¶
func Conflict() Details
Conflict returns a copy of the 409 Conflict problem template.
Example:
return problem.Conflict().Derive(func(details *problem.Builder) {
details.Detail("The group would have no members.")
})
func Forbidden ¶
func Forbidden() Details
Forbidden returns a copy of the 403 Forbidden problem template. Derive an occurrence to add client-safe details without changing the template.
Example:
failure := problem.Forbidden() _ = failure.Status() // 403
func InternalServerError ¶
func InternalServerError() Details
InternalServerError returns a copy of the 500 Internal Server Error problem template. Its stored type is about:blank, matching an equivalent New value.
Example:
failure := problem.InternalServerError() _ = failure.Status() // 500
func New ¶
func New(configure ...BuilderFunc) Details
New invokes callbacks directly, synchronously, in order on one draft. Omitted callbacks use defaults; supplied nil callbacks panic. After callbacks, zero status becomes 500, empty type becomes about:blank, and empty title uses the final status phrase. Retained builders cannot modify the published value.
Example:
failure := problem.New(func(details *problem.Builder) {
details.Status(http.StatusNotFound)
})
_ = failure.Title() // "Not Found"
func NotFound ¶
func NotFound() Details
NotFound returns a copy of the 404 Not Found problem template. Its title is retained when deriving an occurrence with another status.
Example:
failure := problem.NotFound() _ = failure.Status() // 404
func Unauthorized ¶
Unauthorized returns a 401 Unauthorized problem with the supplied HTTP authentication challenge. Callers must provide a non-empty, syntactically valid challenge; it is preserved without parsing or rewriting. The returned value owns its header slices; the shared template is unchanged.
Example:
failure := problem.Unauthorized(`Basic realm="api"`) _ = failure.Status() // 401
func UnprocessableContent ¶
func UnprocessableContent() Details
UnprocessableContent returns a copy of the 422 Unprocessable Content problem template. Its explicit title is Unprocessable Content, rather than the legacy HTTP status phrase Unprocessable Entity.
Example:
failure := problem.UnprocessableContent() _ = failure.Title() // "Unprocessable Content"
func (Details) Defaulted ¶
Defaulted returns a copy with zero-value fields resolved for request. Type defaults to "about:blank", Status to 500, Title to http.StatusText for the status, and Instance to the request URL path. A nil request leaves Instance untouched. The original value is unchanged.
Example:
failure := (problem.Details{}).Defaulted(nil)
_ = failure.Status() // 500
func (Details) Derive ¶
func (problem Details) Derive(configure ...BuilderFunc) Details
Derive applies callbacks in order on one detached draft and publishes a copy. It preserves absent fields and inherited titles, even when status changes. Supplied nil callbacks panic; expanded nil/empty slices supply no callbacks. Only outer extension maps and header slices are copied, not nested values.
Example:
failure := problem.NotFound().Derive(func(details *problem.Builder) {
details.Detail("No order 42").Extension("order_id", 42)
})
_ = failure.Detail()
func (Details) Detail ¶
Detail returns the stored occurrence explanation; empty means absent. It does not apply defaults or mutate the problem.
Example:
value := problem.NotFound().Detail() _ = value
func (Details) Error ¶
Error returns a string representation of the Details for the [error] interface. It uses stored status and falls back to its status phrase for an empty title; a cause is appended for diagnostics, never for JSON rendering.
Example:
message := problem.New().Error() _ = message
func (Details) Extension ¶
Extension returns a retained extension value and whether it exists. Stored nil differs from absence. Nested values remain shared and caller-owned.
Example:
value, found := problem.New().Extension("order_id")
_, _ = value, found
func (Details) Extensions ¶
Extensions returns a copy of the outer extension map, or nil if none exists. Editing this map cannot change details; nested values are not copied.
Example:
values := problem.New().Extensions() _ = values["order_id"]
func (Details) HTTPStatus ¶
HTTPStatus returns the problem's HTTP status code for error handlers. It reports stored status, including zero and unvalidated values. ServeHTTP applies transport bounds and defaults to its response copy, not this value.
Example:
status := problem.NotFound().HTTPStatus() _ = status // 404
func (Details) Instance ¶
Instance returns the stored occurrence URI reference; empty means absent. It does not apply defaults or mutate the problem.
Example:
value := problem.NotFound().Instance() _ = value
func (Details) Is ¶
Is reports whether target is a Details with the same Type, Status and Title as problem, so errors.Is matches a problem against its template. Detail, extension members, headers and the wrapped cause are ignored. The cause is still searched by errors.Is through Details.Unwrap.
Example:
var ErrUserNotFound = problem.NotFound().Derive(func(details *problem.Builder) {
details.Title("User not found")
})
err := ErrUserNotFound.Derive(func(details *problem.Builder) {
details.Detail("no user 42").Extension("id", 42)
})
errors.Is(err, ErrUserNotFound) // true
errors.Is(err, problem.NotFound()) // false: the title differs
func (Details) MarshalJSONTo ¶
MarshalJSONTo delegates the flattened RFC 9457 representation to JSON v2. Zero status is omitted; causes and headers are excluded. Unsupported extension values return the encoder's error without HTTP rendering's fallback. A status extension still conflicts with the standard member when status is omitted, rather than supplying a different advisory status through the map.
Example:
data, err := json.Marshal(problem.New())
if err != nil { panic(err) }
_ = data
func (Details) ServeHTTP ¶
func (problem Details) ServeHTTP(writer http.ResponseWriter, request *http.Request)
ServeHTTP writes the problem response negotiated from request's Accept header. It supports application/problem+json, application/json, and text/plain; a valid header excluding every representation receives 406. It completes missing members and snapshots ResponseContext before encoding. A failed JSON encoding falls back only to an acceptable text representation. A stored status outside net/http's three-digit range (100–999) renders as 500 without changing the stored problem or exposing its wrapped cause. Media parameters must match the emitted representation; text uses UTF-8. Vary includes Accept unless an existing token or wildcard already covers it. Encoding finishes before commitment. A body-write failure stops rendering; this http.Handler interface cannot return that post-commit error to callers.
Example:
recorder := httptest.NewRecorder()
problem.NotFound().ServeHTTP(recorder, httptest.NewRequest("GET", "/orders/42", nil))
_ = recorder.Code
func (Details) Status ¶
Status returns the stored advisory status; zero means unspecified. It does not apply defaults or mutate the problem.
Example:
value := problem.NotFound().Status() _ = value
func (Details) Title ¶
Title returns the stored human-readable category summary. It does not apply defaults or mutate the problem.
Example:
value := problem.NotFound().Title() _ = value
func (Details) Type ¶
Type returns the stored problem-type URI reference. It does not apply defaults or mutate the problem.
Example:
value := problem.NotFound().Type() _ = value
func (*Details) UnmarshalJSONFrom ¶
UnmarshalJSONFrom replaces details only after successful JSON v2 decoding. Absent members stay zero, unknown members become extensions, and success clears previous causes and headers. Failure leaves details unchanged.
Example:
var failure problem.Details
if err := json.Unmarshal([]byte(`{"type":"about:blank","title":"Missing"}`), &failure); err != nil {
panic(err)
}
_ = failure.Status() // 0: absent on the wire
func (Details) Unwrap ¶
Unwrap returns the underlying error from the Details for use with errors.Is and errors.As. A missing cause returns nil; the cause is not copied or serialized.
Example:
failure := problem.New(func(details *problem.Builder) {
details.Error(io.EOF)
})
_ = errors.Is(failure.Unwrap(), io.EOF)
type ResponseContext ¶
type ResponseContext struct {
// contains filtered or unexported fields
}
ResponseContext stores request-scoped RFC 9457 extension members and HTTP response headers. Methods protect outer maps and header slices for concurrent use; nested property values remain shared and require caller coordination, including while ServeHTTP encodes them. The zero value is usable. Do not copy a ResponseContext after first use, because it contains a mutex.
Example:
metadata := problem.NewResponseContext()
metadata.SetProperty("request_id", "req-42")
ctx := problem.NewContext(context.Background(), metadata)
attached, found := problem.FromContext(ctx)
_, _ = attached, found
func FromContext ¶
func FromContext(ctx context.Context) (*ResponseContext, bool)
FromContext retrieves the response context from ctx without panicking when one is absent or has an unexpected type. Nil contexts and missing containers return nil, false. A successful result is the same shared request-local container passed to NewContext.
Example:
ctx := problem.NewContext(context.Background(), problem.NewResponseContext())
if metadata, found := problem.FromContext(ctx); found {
metadata.SetProperty("request_id", "req-42")
}
func NewResponseContext ¶
func NewResponseContext() *ResponseContext
NewResponseContext creates a request-local container with empty property and header maps. Attach it with NewContext; no global state or request is changed.
Example:
metadata := problem.NewResponseContext()
metadata.SetHeader("Cache-Control", "no-store")
_ = metadata.Headers().Get("Cache-Control")
func (*ResponseContext) AddHeader ¶
func (responseContext *ResponseContext) AddHeader(name, value string)
AddHeader appends value to the header field named name. Names are canonicalized using net/http rules. Existing values retain their order; rendering may replace them with explicit problem headers and always owns Content-Type and the required Accept variance.
Example:
metadata := problem.NewResponseContext()
metadata.AddHeader("Vary", "Accept-Language")
_ = metadata.Headers().Values("Vary")
func (*ResponseContext) DeleteHeader ¶
func (responseContext *ResponseContext) DeleteHeader(name string)
DeleteHeader removes the header field named name. Names are canonicalized; removing an absent header is a no-op. It affects only this request's contextual headers, not a published problem's headers.
Example:
metadata := problem.NewResponseContext()
metadata.SetHeader("Retry-After", "60")
metadata.DeleteHeader("retry-after")
_ = metadata.Headers().Get("Retry-After") // ""
func (*ResponseContext) DeleteProperty ¶
func (responseContext *ResponseContext) DeleteProperty(name string)
DeleteProperty removes the RFC 9457 extension member named name. Removing an absent member is a no-op; retained nested values are not mutated or released by this container beyond dropping its own reference.
Example:
metadata := problem.NewResponseContext()
metadata.SetProperty("debug", "temporary")
metadata.DeleteProperty("debug")
_, found := metadata.Properties()["debug"]
_ = found // false
func (*ResponseContext) Headers ¶
func (responseContext *ResponseContext) Headers() http.Header
Headers returns an independent snapshot of response headers. Both the outer map and value slices are copied. Mutating them cannot change this container; an untouched zero ResponseContext returns nil.
Example:
metadata := problem.NewResponseContext()
metadata.AddHeader("Vary", "Accept-Language")
headers := metadata.Headers()
headers.Set("Vary", "Origin") // Does not modify metadata.
func (*ResponseContext) Properties ¶
func (responseContext *ResponseContext) Properties() map[string]any
Properties copies the outer RFC 9457 extension map, not nested values. Editing map entries cannot change this container; mutations through a nested reference can. Coordinate such mutations with all readers and HTTP rendering. An untouched zero ResponseContext returns nil.
Example:
metadata := problem.NewResponseContext()
metadata.SetProperty("request_id", "req-42")
properties := metadata.Properties()
properties["request_id"] = "local" // Does not modify metadata.
func (*ResponseContext) SetHeader ¶
func (responseContext *ResponseContext) SetHeader(name, value string)
SetHeader replaces every value of the header field named name with value. It canonicalizes the name and owns its value slice. Empty values remain present; use DeleteHeader to remove the field instead.
Example:
metadata := problem.NewResponseContext()
metadata.SetHeader("Cache-Control", "no-store")
_ = metadata.Headers().Get("Cache-Control")
func (*ResponseContext) SetProperty ¶
func (responseContext *ResponseContext) SetProperty(name string, value any)
SetProperty replaces the RFC 9457 extension member named name with value. The value is retained, not recursively copied or encoded. Use client-safe, JSON-encodable values; encoding failures follow ServeHTTP's negotiated fallback. Nil is stored, not removed. Standard names are ignored by HTTP rendering.
Example:
metadata := problem.NewResponseContext()
metadata.SetProperty("request_id", "req-42")
_ = metadata.Properties()["request_id"]