problem

package module
v0.0.0-...-46feb1f Latest Latest
Warning

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

Go to latest
Published: Aug 15, 2025 License: MIT Imports: 7 Imported by: 1

README

RFC 9457 Problem Details API for Go

Go Reference Lint Test

This module provides an API for the RFC 9457 problem details specification in Go.

[!WARNING]
This module depends on the experimental github.com/go-json-experiment/json package. This package is planned to become part of the Go standard library in form of a future json/v2 package. Once that happens this module will be updated to use the new json/v2 package from the standard library instead.

Examples

Using problem details

Problem details are represented by the problem.Details type and can be constructed either directly via a struct literal or using the global New function.

Example using a struct literal:

var OutOfCreditProblemType = &problem.Type{
    Type:     "https://example.com/probs/out-of-credit",
    Title:    "You do not have enough credit.",
    Status:   http.StatusForbidden,
    Detail:   "Your current balance is 30, but that costs 50.",
    Instance: "/account/12345/msgs/abc",
    Extensions: map[string]any{
        "balance":  30,
        "accounts": []string{"/account/12345", "/account/67890"},
    },
}

All fields are optional, but at least the Type, Title and Status fields should always be set.

When marshaled as JSON the Details type will result in a single object containing all fields that do not have a zero value as well as all added extensions.

Similarly, when unmarshalling, values for known fields are stored into the existing struct fields, with unknown fields being added into the Extensions map.

The Details object implements the http.Handler interface making it possible to directly write a problem as a response to an HTTP request.

Example:

type (s *MyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // ...
    if outOfCredit {
        (&problem.Details{
            Type:     "https://example.com/probs/out-of-credit",
            Title:    "You do not have enough credit.",
            Status:   http.StatusForbidden,
        }).ServeHTTP(w, r)
        return
    }
    // ...
}
Defining and using reusable types

It is also possible to pre-defines specific problem types that can be used to create new Details instances.

The main use case is as package-level variables that can than be used across different types and functions. These types can reduce boilerplate and serve as part of the documentation

Example:

var OutOfCreditProblemType = &problem.Type{
    URI: "https://example.com/probs/out-of-credit",
    Title: "You do not have enough credit.",
    Status: http.StatusForbidden,
}

The Details method returns a new Details value that inherits the values from the type, while also making it possible to add case specific details data and other relevant data using functional options.

Example:

if outOfCredit {
    OutOfCreditProblemType.Details(
        problem.WithDetail("Your current balance is 30, but that costs 50."),
        problem.WithInstance("/account/12345/msgs/abc"),
        problem.WithExtension("balance", 30),
        problem.WithExtension("accounts", []string{"/account/12345", "/account/67890"}),
    ).ServeHTTP(w, r)
}
Recovering from panics

The Handler function wraps an existing http.Handler and automatically recovers any panics and responds to the request with JSON-encoded problem details.

Example:

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", myHandler)

    // Wrap the handler to handle panics
    handler := problem.Handler(mux)

    log.Fatal(http.ListenAndServe(":8000", handler))
}
Handling problem responses as a client

When making HTTP requests to a server that returns RFC 9457 compatible JSON responses, the From function can be used to extract these errors from the response.

Example:

resp, err := http.Get("https://example.com/")
if err != nil {
	panic(err)
}

defer func() {
	_ = resp.Body.Close()
}()

if resp.StatusCode >= 400 {
    details, err := problem.From(resp)
    if err != nil {
        // the error would not be parsed	
        panic(err)
    }
    
    if details == nil {
        panic("no problem reported")	
    }
    
    // ... handle the error
}

The Is function can be used to check against and handle known problem types.

Example:

switch {
case problem.Is(details, AccountLockedProblemType):
	// handle locked account
case problem.Is(details, OutOfCreditProblemType):
    // handle out of credit
default:
	// handle unknown error
}

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Please make sure to update tests as appropriate.

License

MIT

Documentation

Overview

Package problem implements the RFC 9457 problem details specification in Go.

It also provides some functionality for directly responding to HTTP requests with problems and for defining reusable problem types.

Index

Constants

View Source
const (
	// AboutBlankTypeURI is the default problem type and is equivalent to not specifying a problem type.
	//
	// See also https://datatracker.ietf.org/doc/html/rfc9457#name-aboutblank
	AboutBlankTypeURI = "about:blank"
)
View Source
const (
	// ContentType is the media type used for problem responses, as defined by IANA.
	//
	// See also https://datatracker.ietf.org/doc/html/rfc9457#name-iana-considerations
	ContentType = "application/problem+json"
)

Variables

View Source
var InternalServerError = &Details{
	Status: http.StatusInternalServerError,
	Title:  "Internal Server Error",
}

InternalServerError is used by Handler to serve as response if no callback is defined.

Functions

func Handler

func Handler(next http.Handler) http.Handler

Handler wraps the given http.Handler and automatically recovers panics from given handler.

When recovering from a panic, if the recovered value is an error, the handler will first try converting it into a value of type *Details using errors.As and, if successful, serve the value using Details.ServeHTTP.

Otherwise InternalServerError is served as response.

func Is

func Is(err error, t *Type) bool

Is returns true if the given error can be converted to a *Details using errors.As and the URI, Title and Status match the given type.

If any of [Type.URI], [Type.Title] or [Type.Status] is empty / zero, the field is skipped.

For example, for a type with only a URI and no title or status, only the URI will be compared.

Types

type Details

type Details struct {
	// Type contains the problem type as a URI.
	//
	// If empty, this is the same as "about:blank". See [AboutBlankTypeURI] for more information.
	//
	// See also https://datatracker.ietf.org/doc/html/rfc9457#name-type
	Type string

	// Status is indicating the HTTP status code generated for this occurrence of the problem.
	//
	// This should be the same code as used for the HTTP response and is only advisory.
	//
	// See also https://datatracker.ietf.org/doc/html/rfc9457#name-status
	Status int

	// Title is string containing a short, human-readable summary of the problem type
	//
	// See also https://datatracker.ietf.org/doc/html/rfc9457#name-title
	Title string

	// Detail is string containing a human-readable explanation specific to this occurrence of the problem.
	//
	// See also https://datatracker.ietf.org/doc/html/rfc9457#name-detail
	Detail string

	// Instance is string containing a URI reference that identifies the specific occurrence of the problem
	//
	// See also https://datatracker.ietf.org/doc/html/rfc9457#name-instance
	Instance string

	// Extensions contains any extensions that should be added to the response.
	//
	// If the problem was parsed from a JSON response this will include all extension fields.
	//
	// See also https://datatracker.ietf.org/doc/html/rfc9457#name-extension-members
	Extensions map[string]any

	// Underlying optionally contains the underlying error that lead to / is described by this problem.
	//
	// This field is not part of RFC 9457 and is neither included in generated JSON nor populated during unmarshaling.
	Underlying error
}

Details defines an RFC 9457 problem details object.

Details also implements the [error] interface and can optionally wrap an existing [error] value.

func From

func From(resp *http.Response) (*Details, error)

From returns the problem returned as part of the given HTTP response if any.

As a special case, if [Details.Status] would be 0, it will instead be set to the response status code.

The response body will be closed automatically.

If the response is not of type application/problem+json, the function returns nil, nil and does not close the body.

func New

func New(typ string, title string, status int, opts ...Option) *Details

New returns a new Details instance using the given type, status and title.

It is also possible to set the Detail and Instance fields as well as extensions by providing one or more Option values.

Most users should prefer creating a Details instance via a struct literal or using Type.Details instead.

func (*Details) Error

func (d *Details) Error() string

Error implements the error interface. The returned value is the same as d.Title.

func (*Details) MarshalJSON

func (d *Details) MarshalJSON() ([]byte, error)

MarshalJSON implements the json.Marshaler interface.

See MarshalJSONTo for details.

func (*Details) MarshalJSONTo

func (d *Details) MarshalJSONTo(enc *jsontext.Encoder) error

MarshalJSONTo implements the json.MarshalerTo interface.

If no Type is set, "about:blank" is used. See also AboutBlankTypeURI.

Extension fields named "type", "status", "title", "detail" or "instance" are ignored when marshaling in favor of the respective struct fields even if the field is empty.

func (*Details) ServeHTTP

func (d *Details) ServeHTTP(w http.ResponseWriter, _ *http.Request)

ServeHTTP encodes the value as JSON and writes it to the given response writer.

If encoding fails, no data will be written and ServeHTTP will panic.

ServeHTTP deletes any existing Content-Length header, sets Content-Type to “application/problem+json”, and sets X-Content-Type-Options to “nosniff”.

If set the Status field is used to set the HTTP status. Otherwise http.StatusInternalServerError is used.

ServeHTTP implements the http.Handler interface.

func (*Details) UnmarshalJSON

func (d *Details) UnmarshalJSON(b []byte) error

UnmarshalJSON implements the json.Unmarshaler interface.

See UnmarshalJSONV2 for details.

func (*Details) UnmarshalJSONFrom

func (d *Details) UnmarshalJSONFrom(dec *jsontext.Decoder) error

UnmarshalJSONFrom implements the json.UnmarshalerFrom interface.

As required by RFC 9457 UnmarshalJSONV2 will ignore values for known fields if those values have the wrong type.

For example if the parsed JSON contains a field "status" with the code "400" as a JSON string, the field will be ignored even if it may be possible to parse it as an integer.

func (*Details) Unwrap

func (d *Details) Unwrap() error

Unwrap implements the interface used functions like errors.Is and errors.As to get the underlying error, if any.

type Option

type Option func(*Details)

Option defines functional options that can be used to fill in optional values when creating a Details via New or via Type.Details.

func WithDetail

func WithDetail(detail string) Option

WithDetail sets the Detail for a new Details value.

func WithExtension

func WithExtension(key string, value any) Option

WithExtension adds the given key-value pair to the Extensions of a new Details value.

func WithExtensions

func WithExtensions(extensions map[string]any) Option

WithExtensions adds the values to the Extensions of a new Details value.

func WithInstance

func WithInstance(instance string) Option

WithInstance sets the Instance for a new Details value.

func WithStatus

func WithStatus(status int) Option

WithStatus sets the Status for a new Details value.

func WithUnderlying

func WithUnderlying(err error) Option

WithUnderlying sets the given value as the underlying error of a new Details value.

type Type

type Type struct {
	// URI defines the type URI (typically, with the "http" or "https" scheme)
	URI string

	// Title contains a short, human-readable summary of the problem type.
	Title string

	// Status is the HTTP status code that should be used for responses.
	Status int

	// Extensions contains fixed extensions that are automatically added to Details instances
	// created from this type.
	Extensions map[string]any
}

Type defines a specific problem type that can be used to create new Details instances.

The main use case is as package-level variables that can than be used across different types and functions. These types can reduce boilerplate and serve as part of the documentation.

Example:

var OutOfCreditProblemType = &problem.Type{
	URI: "https://example.com/probs/out-of-credit",
	Title: "You do not have enough credit.",
	Status: http.StatusForbidden,
}

Than in a handler:

type (s *MyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// ...
	if outOfCredit {
		OutOfCreditProblemType.Details().ServeHTTP(w, r)
		return
	}
	// ...
}

When available, extra information can be added using [Option]s:

if outOfCredit {
	OutOfCreditProblemType.Details(
		problem.WithDetail("Your current balance is 30, but that costs 50."),
		problem.WithInstance("/account/12345/msgs/abc"),
		problem.WithExtension("balance", 30),
		problem.WithExtension("accounts", []string{"/account/12345", "/account/67890"}),
	).ServeHTTP(w, r)
	return
}

func (*Type) Details

func (t *Type) Details(opts ...Option) *Details

Details creates a new Details instance from this type.

It is equivalent to calling New(p.URI, p.Status, p.Title, opts...).

Jump to

Keyboard shortcuts

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