A comprehensive, idiomatic, and robust Go library for implementing RFC 7807 (Problem Details for HTTP APIs).
This library provides a standard way to return machine-readable errors from your HTTP APIs, ensuring consistency across your microservices and APIs.
- Standard RFC 7807 Compliance: Supports all standard fields (
type,title,status,detail,instance). - Custom Extensions: Easily add custom arbitrary fields (extension members) that automatically flatten into the JSON root.
- Go 1.13+ Error Wrapping: Implements
Error()andUnwrap()making it fully compatible witherrors.Isanderrors.As. - Pre-defined HTTP Templates: Built-in templates for all common HTTP 4xx and 5xx errors.
- Fluent Options API: Clean, chainable API for building problem details.
- Zero Dependencies: Relies solely on the Go standard library.
go get github.com/semmidev/problemThe easiest way to use the library is with the pre-defined templates in your HTTP handlers.
package main
import (
"net/http"
"github.com/semmidev/problem"
)
func myHandler(w http.ResponseWriter, r *http.Request) {
// Create a problem detail
p := problem.New(
problem.NotFound,
problem.WithDetail("The requested user with id 12345 was not found."),
problem.WithInstance(r.URL.Path),
)
// Write directly to the HTTP response
p.Write(w)
}This automatically sets the Content-Type: application/problem+json header and writes:
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"detail": "The requested user with id 12345 was not found.",
"instance": "/users/12345"
}RFC 7807 allows you to add custom fields to provide domain-specific context.
p := problem.New(
problem.UnprocessableEntity,
problem.WithDetail("You do not have enough credit."),
problem.WithExtension("balance", 30),
problem.WithExtensions(map[string]any{
"currency": "USD",
"account": "/account/12345",
}),
)Generates:
{
"type": "about:blank",
"title": "Unprocessable Entity",
"status": 422,
"detail": "You do not have enough credit.",
"balance": 30,
"currency": "USD",
"account": "/account/12345"
}You are encouraged to define your own problem types for domain-specific errors.
var OutOfCredit = problem.TypeTemplate{
Type: "https://example.com/probs/out-of-credit",
Title: "You do not have enough credit.",
Status: http.StatusForbidden,
}
// Later in a handler:
p := problem.New(OutOfCredit, problem.WithDetail("Current balance is 30, but that costs 50."))Because *problem.Problem implements the error interface, it can be seamlessly passed through your service layer and wrapped.
// In your repository/service layer
func queryDB() error {
err := db.Query(...) // let's imagine this fails
// Wrap the internal error inside a Problem
return problem.Wrap(err, problem.InternalServerError,
problem.WithDetail("Database timeout"),
problem.WithExtension("trace_id", "req-789"),
)
}
// In your HTTP handler
func handler(w http.ResponseWriter, r *http.Request) {
err := queryDB()
if err != nil {
// Use errors.As or the provided IsProblem helper to extract it
if p, ok := problem.IsProblem(err); ok {
p.Write(w)
return
}
// Fallback for unknown errors
problem.New(problem.InternalServerError).Write(w)
}
}Check out the example/kanban directory for a complete, runnable example of using the problem library.
It features:
- Clean Architecture: Clear separation of domain rules, repositories, and HTTP handlers.
- Clear Error Boundaries: Mapping standard domain errors to RFC 7807 problem details.
- Go Chi & Govalidator: Demonstrates using the library with widely used community packages, mapping validation errors to
422 Unprocessable Entityproblem extensions.