See examples/ for extra implementation information for this library.
Suggested Implementation
package main
import (
"net/http"
"alpineworks.io/rfc9457"
)
func main() {
http.Handle("/test", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
rfc9457.NewRFC9457(
rfc9457.WithStatus(http.StatusTeapot),
rfc9457.WithDetail("I'm a teapot"),
rfc9457.WithTitle("Test endpoint hit - I'm a teapot"),
rfc9457.WithInstance("/test"),
).ServeHTTP(w, req)
}))
http.ListenAndServe(":8080", nil)
}curl localhost:8080/test
{
"type": "about:blank",
"status": 418,
"title": "Test endpoint hit - I'm a teapot",
"detail": "I'm a teapot",
"instance": "/test"
}Custom problem type
Type defaults to about:blank. Use WithType to point at a documented problem-type URI:
rfc9457.NewRFC9457(
rfc9457.WithType("https://example.com/problems/insufficient-funds"),
rfc9457.WithStatus(http.StatusForbidden),
rfc9457.WithTitle("You do not have enough credit"),
rfc9457.WithDetail("Your current balance is 30, but charge was 50"),
rfc9457.WithInstance("/account/12345/transactions/abc"),
).ServeHTTP(w, req)Per RFC 9457, when decoding a problem with a missing type, FromJSON populates Type with about:blank.
Status-named constructors
For common HTTP problems, the status-named helpers seed Status and a default Title from http.StatusText. Layer on detail/instance/extensions as needed:
rfc9457.NotFound(
rfc9457.WithDetail("user 42 not found"),
rfc9457.WithInstance("/users/42"),
rfc9457.WithExtensions(rfc9457.NewExtension("user_id", 42)),
).ServeHTTP(w, req)Available helpers: BadRequest, Unauthorized, Forbidden, NotFound, MethodNotAllowed, Conflict, Gone, UnprocessableEntity, TooManyRequests, InternalServerError, NotImplemented, BadGateway, ServiceUnavailable, GatewayTimeout.
Extensions
Extension members flatten into the top-level JSON object on encode and are pulled back into Extensions on decode:
rfc9457.NewRFC9457(
rfc9457.WithStatus(http.StatusBadRequest),
rfc9457.WithTitle("Validation failed"),
rfc9457.WithExtensions(
rfc9457.NewExtension("trace_id", "abc-123"),
rfc9457.NewExtension("invalid_params", []map[string]string{
{"name": "email", "reason": "must be a valid email"},
}),
),
).ServeHTTP(w, req)WithExtensions is additive across calls — each invocation merges into the existing map (later keys overwrite earlier ones), so option helpers can compose cleanly.
Extension keys that collide with reserved members (type, status, title, detail, instance) cause ToJSON / MarshalJSON to return ErrExtensionKeyCollision.
Wrapping a Go error
FromError builds a problem with Title from http.StatusText(status) and Detail from err.Error():
if err := doWork(); err != nil {
rfc9457.FromError(err, http.StatusInternalServerError).ServeHTTP(w, req)
return
}*RFC9457 also implements the error interface, so a problem can be returned and inspected as a Go error.
Encoding and decoding
// Encode
problem := rfc9457.BadRequest(rfc9457.WithDetail("missing field 'email'"))
body, err := problem.ToJSON() // []byte
// Decode
parsed, err := rfc9457.FromJSON(body)*RFC9457 also implements json.Marshaler and json.Unmarshaler, so it works directly with encoding/json.
Sentinel errors returned by this package: ErrUnableToMarshalJSON, ErrUnableToUnmarshalJSON, ErrExtensionKeyCollision. Use errors.Is to check them.
HTTP serving behavior
*RFC9457 implements http.Handler. ServeHTTP:
- Sets
Content-Typetoapplication/problem+json(also exposed asrfc9457.ProblemContentType). - Writes
r.Statusas the response code, defaulting to500 Internal Server ErrorwhenStatusis unset. - If serialization fails (for example, an extension key collides with a reserved field), writes a minimal fallback problem body with status 500 instead of leaving the response empty.
