Help us shape the future of Go! Take the 2026 Go Developer Survey

problem

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 11 Imported by: 0

README

Arkoren Problem

Clients need a consistent error format rather than a new response shape for each endpoint. arkoren.dev/foundation/problem is a dependency-free Go implementation of RFC 9457, Problem Details for HTTP APIs. A Details value implements both error and http.Handler, so it can represent and serve a structured HTTP error response.

Installation

go get arkoren.dev/foundation/problem

Usage

Define a reusable error, then derive request-specific details without changing the template. Import arkoren.dev/foundation/problem and the standard-library net/http package:

var ErrNotFound = problem.New(func(details *problem.Builder) {
	details.Type("https://api.example.com/problems/not-found")
	details.Title("Resource Not Found")
	details.Status(http.StatusNotFound)
})

func handler(w http.ResponseWriter, r *http.Request) {
	ErrNotFound.Derive(func(details *problem.Builder) {
		details.Extension("resource_id", "123")
	}).ServeHTTP(w, r)
}

The handler serves HTTP 404 with a resource_id extension. Store or return construction callbacks as problem.BuilderFunc, an alias for func(*problem.Builder). Both New and Details.Derive accept this type.

Only put client-safe text in titles, details, and extensions. Attach internal causes with details.Error(err) so errors.Is can inspect them without exposing their text in the response.

New() defaults status to 500, type to about:blank, and an empty title to the final status phrase after all callbacks. Derive inherits stored fields without normalization: changing status does not replace an inherited title. Read standard members with Type(), Title(), Status(), Detail(), and Instance().

When served, unset Type, Status, Title, and Instance members default to "about:blank", 500, its HTTP status text, and the request URL path, respectively. Detail is never inferred from a wrapped error.

Callbacks run directly, once, synchronously, in order on one draft. Omitted or expanded nil/empty callback slices request no edits; a supplied nil callback panics. Retaining a builder cannot modify the published value. Outer extension maps and header slices are copied, but arbitrary nested extension values remain shared. Extension(name) returns a value and presence flag (including stored nil); Extensions() returns an independent outer map.

JSON decoding preserves missing status as zero and replaces the destination only on success, clearing old causes and headers. Plain JSON omits zero status; HTTP rendering completes defaults and includes a status matching the response code. Construction and decoding retain nonzero statuses without validation. When a stored status falls outside net/http's three-digit range (100–999), only ServeHTTP renders it as 500. Accessors, plain JSON, Defaulted, and errors.Is still observe the original stored identity.

Status problems

Status helpers such as NotFound() use New, so their stored type is about:blank, not an absent type. They match equivalent New constructions through errors.Is, which compares the exact stored type, status, and title. Deriving a cause or detail does not change that identity. Raw zero values and decoded absent members still retain their absence until HTTP defaulting.

Standard HTTP status problems are directly returnable from an Arkoren handler or servable as an http.Handler:

func handler(w http.ResponseWriter, r *http.Request) error {
	return problem.NotFound()
}

func protected(w http.ResponseWriter, r *http.Request) {
	problem.Unauthorized(`Basic realm="api"`).ServeHTTP(w, r)
}

problem.Unauthorized requires a caller-supplied, non-empty, syntactically valid HTTP authentication challenge. It preserves that challenge in the WWW-Authenticate response header.

Response headers are immutable, non-serialized response state. In a Derive callback, use Header to replace copied values, AddHeader to append a value, or RemoveHeader to remove it. Header(name) also removes it:

problem.BadRequest().Derive(func(details *problem.Builder) {
	details.Header("Cache-Control", "no-store")
	details.AddHeader("Vary", "Accept-Language")
})

ServeHTTP owns Content-Type; it replaces any stored value with the negotiated Problem Details, JSON, or text content type. It also adds Accept to Vary after applying your headers, unless an existing case-insensitive Accept token or * already covers negotiation. Other Vary values are retained.

Content negotiation

ServeHTTP considers application/problem+json, application/json, and text/plain, in that server preference order. When no syntactically valid Accept value is supplied, it selects application/problem+json. For a valid header, the most-specific matching range controls a representation; an explicit q=0 excludes it even when a less-specific wildcard permits it. If the client excludes every supported representation, ServeHTTP sends HTTP 406 without rendering the original problem. If a JSON extension value cannot be marshaled, the response falls back to plain text only when text/plain is acceptable; otherwise it sends HTTP 406.

An omitted q means quality 1; a malformed weight invalidates that range. Weights use HTTP's 0–1 grammar with at most three decimal places, not arbitrary floating-point notation. If every range is invalid, the normal server order applies. Media parameters must match the actual representation. The text renderer emits text/plain; charset=utf-8, so another required charset is not acceptable. A parameterized matching range outranks an otherwise equal plain range, including a parameterized q=0 exclusion. Unprovided JSON profile parameters do not match either JSON renderer.

Encoding finishes before response headers are committed. A body-write error stops rendering without attempting another representation after commitment; the http.Handler interface cannot return that write error to the caller.

Validation errors

problem has no validation type. arkoren.dev/foundation/validate produces validate.Errors, and request.ValidationProblem in the core module turns them into a 422 problem with an errors extension. The Arkoren router does not render a raw validate.Errors; it answers 500.

Request response context

Middleware can attach one mutable ResponseContext with RFC 9457 extension members and baseline response headers. Create it, attach it to the request, and then mutate it with per-field methods:

Here incoming is the existing *http.Request passed to your middleware:

metadata := problem.NewResponseContext()
metadata.SetProperty("request_id", "123")
metadata.SetHeader("Cache-Control", "no-store")
metadata.AddHeader("Vary", "Accept-Language")

incoming = incoming.WithContext(problem.NewContext(incoming.Context(), metadata))

Properties() copies only the outer map. Replacing an entry in that snapshot cannot change the context, but mutating a nested map or slice can. Coordinate nested mutation with all readers, including response encoding; the container's mutex does not protect nested values. Headers() copies both the outer http.Header and its value slices. Do not copy a ResponseContext after use.

When served, standard RFC 9457 member names (type, title, detail, status, and instance) from a response context are ignored. Explicit Builder.Extension values take precedence over same-named context properties. Context headers are written first; Builder.Header and Builder.AddHeader replace every contextual value for the same field. ServeHTTP owns Content-Type and adds the required Accept variance after these replacements, so contextual or problem headers cannot disable either.

Features

  • RFC 9457 standard members plus top-level extensions via Builder.Extension.
  • Immutable published values, allowing reusable package-level problem templates.
  • Wrapped native errors via Builder.Error, available through errors.Is and errors.As but never serialized.
  • errors.Is(err, template) matches a problem with the same type, status and title as template, ignoring detail, members, headers and cause.
  • Content negotiation for application/problem+json, application/json, and a plain-text fallback.
  • Request-scoped properties and response headers through ResponseContext.

Development

Go 1.27 or later is required. Run the checks directly:

go test -race ./...
go vet ./...
govulncheck ./...

License

MIT. See LICENSE.

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

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

func (details *Builder) AddHeader(name, value string) *Builder

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

func (details *Builder) Detail(value string) *Builder

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

func (details *Builder) Error(err error) *Builder

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

func (details *Builder) Extension(name string, value any) *Builder

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

func (details *Builder) Header(name string, values ...string) *Builder

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

func (details *Builder) Instance(value string) *Builder

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

func (details *Builder) RemoveExtension(name string) *Builder

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

func (details *Builder) RemoveHeader(name string) *Builder

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

func (details *Builder) Status(value int) *Builder

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

func (details *Builder) Title(value string) *Builder

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

func (*Builder) Type

func (details *Builder) Type(value string) *Builder

Type replaces the URI reference without validation and returns the same draft. Empty defaults only in New, not Derive.

Example:

failure := problem.New(func(details *problem.Builder) {
	details.Type("urn:example:missing")
})
_ = failure.Type()

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

func Unauthorized(challenge string) Details

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

func (problem Details) Defaulted(request *http.Request) Details

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

func (problem Details) Detail() string

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

func (problem Details) Error() string

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

func (problem Details) Extension(name string) (any, bool)

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

func (problem Details) Extensions() map[string]any

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

func (problem Details) HTTPStatus() int

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

func (problem Details) Instance() string

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

func (problem Details) Is(target error) bool

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

func (problem Details) MarshalJSONTo(encoder *jsontext.Encoder) error

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

func (problem Details) Status() int

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

func (problem Details) Title() string

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

func (problem Details) Type() string

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

func (problem *Details) UnmarshalJSONFrom(decoder *jsontext.Decoder) error

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

func (problem Details) Unwrap() error

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"]

Directories

Path Synopsis
Package internal provides non-exported helpers for the problem module.
Package internal provides non-exported helpers for the problem module.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL