Package rfc9457
Overview
Package rfc9457 builds and reads problem details, defined by RFC 9457 (Problem Details for HTTP APIs): a JSON body, with the media type application/problem+json, that explains why a request failed.
Servers
A Problem starts from an rfc9110.Status, whose code it uses for the response and whose header fields it sends, so a 401 problem carries its WWW-Authenticate challenge:
rfc9457.New(rfc9110.Unauthorized(challenge)).
WithDetail("The access token has expired.").
Write(w)Declare each problem type your API uses once, as you would a sentinel error, and add it with Problem.WithType:
var outOfCredit = rfc9457.MustType("https://example.com/probs/out-of-credit", "You do not have enough credit.")
rfc9457.New(rfc9110.Forbidden()).WithType(outOfCredit)A problem without a type is "about:blank", and its title is the status code's reason phrase (Section 4.2.1).
Clients
Decode reads a problem from a response. Problem implements error, and Problem.HasType maps it back to the problem types you know:
p, err := rfc9457.Decode(resp)
if err == nil && p.HasType(outOfCredit) {
return ErrOutOfCredit
}The package doesn't cover the XML format of Appendix B.
The RFC text is in rfc9457.txt in this directory, and the doc comments cite its sections.
Index
- Constants
- Variables
type ExtensionNametype FieldErrortype Problemfunc Decodefunc Newfunc (Problem) Detailfunc (Problem) Errorfunc (Problem) Errorsfunc (Problem) Extensionfunc (Problem) HasTypefunc (Problem) Instancefunc (Problem) MarshalJSONfunc (Problem) Statusfunc (Problem) Titlefunc (Problem) Typefunc (Problem) UnmarshalJSONfunc (Problem) WithDetailfunc (Problem) WithErrorsfunc (Problem) WithExtensionfunc (Problem) WithInstancefunc (Problem) WithTypefunc (Problem) Write
type Type
Constants
const MediaType = "application/problem+json"MediaType is the media type of a problem details JSON object (Section 3).
Variables
var (
// ErrInvalidType is returned by [NewType] for a URI that's empty or
// isn't a URI reference.
ErrInvalidType = errors.New("invalid problem type")
// ErrInvalidExtensionName is returned by [ParseExtensionName] for a
// name that doesn't follow Section 4, or is a member this package
// defines.
ErrInvalidExtensionName = errors.New("invalid extension name")
// ErrInvalidStatus is returned by [Problem.Write] for a Problem that
// wasn't built by [New] or [Decode], so has no status code to send.
ErrInvalidStatus = errors.New("invalid status")
// ErrNotProblem is returned by [Decode] for a response whose media type
// isn't application/problem+json.
ErrNotProblem = errors.New("not a problem details response")
// ErrTooLarge is returned by [Decode] for a body over 1 MiB.
ErrTooLarge = errors.New("problem details too large")
)Types
ExtensionName source
type ExtensionName stringExtensionName is the name of an extension member. Use ParseExtensionName to check a string.
See RFC 9457, Section 3.2.
ParseExtensionName source
func ParseExtensionName(s string) (ExtensionName, error)ParseExtensionName returns s as an ExtensionName. Following Section 4, it must start with a letter, contain only letters, digits and "_", and be at least three characters long. It mustn't be a standard member (Section 3.1) or "errors", which Problem.WithErrors sets. Otherwise it returns ErrInvalidExtensionName.
FieldError source
type FieldError struct {
Detail string
Pointer rfc6901.Pointer
}FieldError describes one problem with a request's content, such as an invalid field. It's an entry in a problem's "errors" member, which follows the example in Section 3. Its pointer is sent in URI fragment form, such as "#/age".
(FieldError) MarshalJSON source
func (e FieldError) MarshalJSON() ([]byte, error)MarshalJSON returns the field error as an object with "detail" and "pointer" members.
(FieldError) UnmarshalJSON source
func (e *FieldError) UnmarshalJSON(data []byte) errorUnmarshalJSON reads a field error. It accepts a pointer in either URI fragment form ("#/age") or string form ("/age").
Problem source
type Problem struct {
// contains filtered or unexported fields
}Problem is a problem details object. Build one with New, or read one with Decode. The zero Problem has no status, so Problem.Write rejects it.
Problem is immutable: its With methods return a modified copy. It implements error, so a client can return the problem it received.
See RFC 9457, Section 3.
Decode source
func Decode(resp *http.Response) (Problem, error)Decode reads a problem from a response, without closing its body. It
returns ErrNotProblem if the media type isn't application/problem+json,
and ErrTooLarge if the body is over 1 MiB. If the body has no "status"
member, the problem takes the response's status code. ExampleDecode shows a server writing a problem and a client decoding it,
reading an extension, and mapping its type back to a sentinel error. Output:Example
package main
import (
"errors"
"fmt"
"net/http"
"net/http/httptest"
"funnyhaha.technology/rfc/rfc9110"
"funnyhaha.technology/rfc/rfc9457"
)
var errOutOfCredit = errors.New("out of credit")
func main() {
outOfCredit := rfc9457.MustType("https://example.com/probs/out-of-credit", "You do not have enough credit.")
balance, err := rfc9457.ParseExtensionName("balance")
if err != nil {
panic(err)
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_ = rfc9457.New(rfc9110.Forbidden()).
WithType(outOfCredit).
WithDetail("Your current balance is 30, but that costs 50.").
WithExtension(balance, 30).
Write(w)
}))
defer srv.Close()
resp, err := http.Get(srv.URL) //nolint:noctx // An example, with no context to pass.
if err != nil {
panic(err)
}
defer resp.Body.Close()
p, err := rfc9457.Decode(resp)
if err != nil {
panic(err)
}
var amount int
_, err = p.Extension("balance", &amount)
if err != nil {
panic(err)
}
err = p
if p.HasType(outOfCredit) {
err = fmt.Errorf("%w: balance %d", errOutOfCredit, amount)
}
fmt.Println(errors.Is(err, errOutOfCredit), err)
}
true out of credit: balance 30
New source
func New(s rfc9110.Status) ProblemNew returns an "about:blank" problem for the status. It sends the status's code and header fields, and its title is the code's reason phrase, as Section 4.2.1 recommends.
(Problem) Detail source
func (p Problem) Detail() stringDetail returns the explanation specific to this occurrence.
(Problem) Error source
func (p Problem) Error() stringError returns the status code, title and detail, such as "403 You do not have enough credit.: Your current balance is 30".
(Problem) Errors source
func (p Problem) Errors() []FieldErrorErrors returns a copy of the problem's field errors.
(Problem) Extension source
func (p Problem) Extension(name string, v any) (bool, error)Extension unmarshals the extension member with the name into v, as json.Unmarshal does. It reports false if p has no such member.
(Problem) HasType source
func (p Problem) HasType(t Type) boolHasType reports whether p is a problem of type t.
(Problem) Instance source
func (p Problem) Instance() stringInstance returns the URI reference of this occurrence.
(Problem) MarshalJSON source
func (p Problem) MarshalJSON() ([]byte, error)MarshalJSON returns p as a JSON object. It leaves out empty members, and "type" when it's "about:blank" (Section 3.1.1), and writes extension members after the standard ones, sorted by name.
(Problem) Status source
func (p Problem) Status() intStatus returns the problem's status code.
(Problem) Title source
func (p Problem) Title() stringTitle returns the problem's title.
(Problem) Type source
func (p Problem) Type() stringType returns the problem type's URI, which is "about:blank" if it has none.
(Problem) UnmarshalJSON source
func (p *Problem) UnmarshalJSON(data []byte) errorUnmarshalJSON reads a problem details object. A standard member with the wrong JSON type is ignored, as Section 3.1 requires, and every other member is kept as an extension.
(Problem) WithDetail source
func (p Problem) WithDetail(detail string) ProblemWithDetail returns a copy of p with an explanation specific to this occurrence of the problem. It should help the client correct the problem, rather than give debugging information (Section 3.1.4).
(Problem) WithErrors source
func (p Problem) WithErrors(errs ...FieldError) ProblemWithErrors returns a copy of p with the field errors added to its
"errors" member. ExampleProblem_WithErrors shows the validation-error example of RFC 9457,
Section 3, with a JSON Pointer to each invalid field. Output:Example
package main
import (
"fmt"
"funnyhaha.technology/rfc/rfc6901"
"funnyhaha.technology/rfc/rfc9110"
"funnyhaha.technology/rfc/rfc9457"
)
func main() {
validation := rfc9457.MustType("https://example.net/validation-error", "Your request is not valid.")
p := rfc9457.New(rfc9110.UnprocessableContent()).
WithType(validation).
WithErrors(
rfc9457.FieldError{Detail: "must be a positive integer", Pointer: rfc6901.New("age")},
rfc9457.FieldError{Detail: "must be 'green', 'red' or 'blue'", Pointer: rfc6901.New("profile", "color")},
)
body, err := p.MarshalJSON()
if err != nil {
panic(err)
}
fmt.Println(string(body))
}
{"type":"https://example.net/validation-error","title":"Your request is not valid.","status":422,"errors":[{"detail":"must be a positive integer","pointer":"#/age"},{"detail":"must be 'green', 'red' or 'blue'","pointer":"#/profile/color"}]}
(Problem) WithExtension source
func (p Problem) WithExtension(name ExtensionName, value any) ProblemWithExtension returns a copy of p with an extension member, replacing any with the same name. The value is marshalled when p is written, so Problem.Write and Problem.MarshalJSON return an error if it can't be.
(Problem) WithInstance source
func (p Problem) WithInstance(instance string) ProblemWithInstance returns a copy of p with a URI reference that identifies this occurrence of the problem (Section 3.1.5).
(Problem) WithType source
func (p Problem) WithType(t Type) ProblemWithType returns a copy of p with the problem type, and the type's title. The zero Type sets "about:blank" and the status code's reason phrase.
(Problem) Write source
func (p Problem) Write(w http.ResponseWriter) errorWrite sends p as the response: its status's header fields, a
Content-Type of application/problem+json, the status code and the JSON
body. If p can't be marshalled, Write returns an error before writing
anything, so the caller can still send another response. ExampleProblem_Write shows a 401 problem that carries an RFC 6750 Bearer
challenge, so the client gets both the challenge and a JSON explanation. Output:Example
package main
import (
"fmt"
"net/http/httptest"
"funnyhaha.technology/rfc/rfc6750"
"funnyhaha.technology/rfc/rfc9457"
)
func main() {
bearer := rfc6750.MustChallenge("api")
rec := httptest.NewRecorder()
err := rfc9457.New(rfc6750.InvalidToken(bearer, "The access token expired")).
WithDetail("Sign in again to get a new token.").
Write(rec)
if err != nil {
panic(err)
}
fmt.Println(rec.Code)
fmt.Println(rec.Header().Get("Content-Type"))
fmt.Println(rec.Header().Get("WWW-Authenticate"))
fmt.Println(rec.Body)
}
401
application/problem+json
Bearer realm="api", error="invalid_token", error_description="The access token expired"
{"title":"Unauthorized","status":401,"detail":"Sign in again to get a new token."}
Type source
type Type struct {
// contains filtered or unexported fields
}Type is a problem type: a URI that identifies it, and a title that summarises it. The zero Type is "about:blank", which means the problem has no semantics beyond its status code.
Declare each type once, with MustType for a package-level variable or NewType, as you would a sentinel error.
See RFC 9457, Sections 3.1.1 and 4.
MustType source
func MustType(uri, title string) TypeMustType is like NewType but panics if uri isn't valid. Use it to declare a package-level problem type.
NewType source
func NewType(uri, title string) (Type, error)NewType returns a problem type. The uri should be absolute (Section 3.1.1). It returns ErrInvalidType if uri is empty or isn't a URI reference.
(Type) Title source
func (t Type) Title() stringTitle returns the type's title.
(Type) URI source
func (t Type) URI() stringURI returns the type's URI.