You are reading the 0.3.0 documentation. The current release is 0.3.1. Read this page for it →

Problem Details

RFC 9457, "Problem Details for HTTP APIs", is the standard shape for an error body, and the one an API gateway, a client generator or another team's service may already expect. Muzak's default envelope stays the default; one option switches the whole application to problems.

app := muzak.New(muzak.AppOptions{
    Title: settings.AppName,
    // Every error is answered as application/problem+json, and the OpenAPI
    // document describes every error response that way.
    ProblemDetails: &muzak.ProblemOptions{TypeBase: "https://errors.example.com/"},
})
{
  "type": "https://errors.example.com/validation_error",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "The request could not be validated.",
  "instance": "urn:uuid:0611f4b2-2f0a-7b57-9c1a-6e6a2e2f9b31",
  "code": "validation_error",
  "errors": [
    { "field": "name", "location": "body", "issue": "is required" },
    { "field": "limit", "location": "query", "issue": "must be a valid integer" }
  ],
  "request_id": "0611f4b2-2f0a-7b57-9c1a-6e6a2e2f9b31"
}

The members

MemberCarries
typeTypeBase followed by the error code, path-escaped; about:blank when no base is set
titlethe status's reason phrase, such as Not Found, translated where the application translates it
statusthe HTTP status
detailthe message the default envelope would carry, held to the same rule
instanceurn:uuid: followed by the request identifier
codethe stable classifier, such as not_found (extension)
errorsevery field that failed, as the envelope's details (extension, omitted when empty)
request_idthe identifier that joins the response to the server's log (extension)

The Go type is muzak.Problem, and the media type muzak.ProblemContentType.

instance names the occurrence by its request identifier rather than by the request's path. A path can hold personal data, an email address or a name, that a problem report forwarded to a ticket or a log aggregator should not repeat. Every identifier Muzak assigns is a UUID, so the member is a URN of the registered uuid namespace; an application that assigns identifiers of its own that are not UUIDs gets no instance.

code, errors and request_id are what the default envelope carries, so a client moving from one format to the other loses nothing. Branch on code, or on type when a base is set; never on title or detail, which are text for people and are translated.

TypeBase

muzak.ProblemOptions{TypeBase: "https://errors.example.com/"}   // https://errors.example.com/not_found
muzak.ProblemOptions{TypeBase: "urn:example:errors:"}           // urn:example:errors:not_found
muzak.ProblemOptions{}                                          // about:blank

The code is appended as it is, so the base ends in whatever separates the two. A code with a character a path segment cannot carry, as one set with WithCode("card declined") might, is escaped: card%20declined. The RFC recommends serving documentation at each type URI; Muzak does not, so publish it there if you name a base that resolves.

Left empty, every problem's type is about:blank, which the RFC defines as a problem described by its status alone, and the title is then the status's reason phrase, as the RFC asks.

A base that is not an absolute URI, or that carries a query, a fragment, whitespace or a character outside ASCII, is a build error: a client would resolve a relative one against wherever it read the response, and a code appended after a ? or # would land in the wrong part.

The same promises as the envelope

The problem renderer decides nothing on its own. It asks DefaultErrorRenderer and reshapes what that decided, so everything Error Handling says about the envelope holds for a problem too:

  • The same status and code for every error, and the same details for a validation failure.
  • A server-side fault, a plain error, a StatusCoder at 500 or above, or a panic, is answered with the fixed sentence The server could not complete the request. while the cause is logged and never sent. An *HTTPError keeps the message it was written with, at any status.
  • The headers set before the error was rendered are kept: Allow on a 405, Retry-After on a 429, WWW-Authenticate on a 401, Vary, Set-Cookie. The body's own headers are reset and the response is sent with Cache-Control: no-store.

Every error the application produces is a problem, not only those a route returns: a refused host, a rate limit, a guard on a mounted handler, an unknown path, a method the documentation does not answer, the 405 of a health endpoint, and a panic in middleware installed with App.Use, which used to be answered with the fixed envelope whatever the renderer was. So is the 500 an application that failed to build answers every request with, and the fixed 500 that stands in when an error renderer's body cannot be encoded; neither has a request to translate for, so both are in English.

The same goes for the features that refuse a request before any route runs. A state-changing request a browser sends from another origin is refused by Cross-Origin Requests with a 403 problem whose code is cross_origin_request, and a session that cannot be saved fails its request through the renderer, as a handler's error would.

Translation

detail is translated exactly as the envelope's message is (see Internationalization). title is looked up at muzak.status.<code>, falling back to the English phrase net/http knows the status by:

es:
  muzak:
    status:
      404: "No encontrado"
      422: "Entidad no procesable"
    http:
      404: "No se encontro el recurso solicitado."

The shipped locale carries the English titles of every status the framework classifies, and a test keeps them equal to http.StatusText, so turning a store on changes no English title. The machine members, type, code and status, are never translated.

The document

With ProblemDetails set, every error response the OpenAPI document describes, the 422 of a route that binds input, the 429 of a rate-limited one, the 503 of a route with a Timeout, each WithResponseDoc and the default, is application/problem+json with a $ref to the Problem component, and the ErrorResponse component is no longer emitted. A response declared with WithResponseModel keeps its own model.

Composing with a renderer of your own

muzak.ProblemDetails(opts) is the renderer itself, so an application with errors of its own to render can delegate the rest to it:

problems := muzak.ProblemDetails(muzak.ProblemOptions{TypeBase: "https://errors.example.com/"})

app := muzak.New(muzak.AppOptions{
    ProblemDetails: &muzak.ProblemOptions{TypeBase: "https://errors.example.com/"},
    ErrorRenderer: func(ctx *muzak.Context, err error) (int, any) {
        var declined *billing.DeclinedError
        if errors.As(err, &declined) {
            ctx.SetHeader("Content-Type", muzak.ProblemContentType)
            return http.StatusPaymentRequired, DeclinedProblem{ /* ... */ }
        }
        return problems(ctx, err)
    },
})

When ErrorRenderer is set, it is used, and ProblemDetails still tells the document that errors are problems. Set both, as above: installing muzak.ProblemDetails through ErrorRenderer alone renders problems but leaves the document describing the envelope.

Limits

A few answers are written by net/http's file serving rather than by the renderer, and are plain text whatever the format:

  • The 416 that http.ServeContent sends for a range a file cannot satisfy, on a static file or frontend mount and for a FileResponse.
  • The 404 or 500 a static file or frontend mount sends when a file it found cannot be opened or read a moment later, for instance because it was deleted in between. The cause is logged.

Where to go next

Error Handling covers which errors describe themselves and which stay opaque, and OpenAPI what else the document says about a route.

Open source under MIT / Apache-2.0.Built on net/http, and nothing else.