|
| 1 | +# problem |
| 2 | + |
| 3 | +[](https://pkg.go.dev/github.com/semmidev/problem) |
| 4 | + |
| 5 | +A comprehensive, idiomatic, and robust Go library for implementing **RFC 7807 (Problem Details for HTTP APIs)**. |
| 6 | + |
| 7 | +This library provides a standard way to return machine-readable errors from your HTTP APIs, ensuring consistency across your microservices and APIs. |
| 8 | + |
| 9 | +## Features |
| 10 | + |
| 11 | +- **Standard RFC 7807 Compliance**: Supports all standard fields (`type`, `title`, `status`, `detail`, `instance`). |
| 12 | +- **Custom Extensions**: Easily add custom arbitrary fields (extension members) that automatically flatten into the JSON root. |
| 13 | +- **Go 1.13+ Error Wrapping**: Implements `Error()` and `Unwrap()` making it fully compatible with `errors.Is` and `errors.As`. |
| 14 | +- **Pre-defined HTTP Templates**: Built-in templates for all common HTTP 4xx and 5xx errors. |
| 15 | +- **Fluent Options API**: Clean, chainable API for building problem details. |
| 16 | +- **Zero Dependencies**: Relies solely on the Go standard library. |
| 17 | + |
| 18 | +## Installation |
| 19 | + |
| 20 | +```bash |
| 21 | +go get github.com/semmidev/problem |
| 22 | +``` |
| 23 | + |
| 24 | +## Basic Usage |
| 25 | + |
| 26 | +The easiest way to use the library is with the pre-defined templates in your HTTP handlers. |
| 27 | + |
| 28 | +```go |
| 29 | +package main |
| 30 | + |
| 31 | +import ( |
| 32 | + "net/http" |
| 33 | + "github.com/semmidev/problem" |
| 34 | +) |
| 35 | + |
| 36 | +func myHandler(w http.ResponseWriter, r *http.Request) { |
| 37 | + // Create a problem detail |
| 38 | + p := problem.New( |
| 39 | + problem.NotFound, |
| 40 | + problem.WithDetail("The requested user with id 12345 was not found."), |
| 41 | + problem.WithInstance(r.URL.Path), |
| 42 | + ) |
| 43 | + |
| 44 | + // Write directly to the HTTP response |
| 45 | + p.Write(w) |
| 46 | +} |
| 47 | +``` |
| 48 | + |
| 49 | +This automatically sets the `Content-Type: application/problem+json` header and writes: |
| 50 | + |
| 51 | +```json |
| 52 | +{ |
| 53 | + "type": "about:blank", |
| 54 | + "title": "Not Found", |
| 55 | + "status": 404, |
| 56 | + "detail": "The requested user with id 12345 was not found.", |
| 57 | + "instance": "/users/12345" |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +## Adding Custom Extensions |
| 62 | + |
| 63 | +RFC 7807 allows you to add custom fields to provide domain-specific context. |
| 64 | + |
| 65 | +```go |
| 66 | +p := problem.New( |
| 67 | + problem.UnprocessableEntity, |
| 68 | + problem.WithDetail("You do not have enough credit."), |
| 69 | + problem.WithExtension("balance", 30), |
| 70 | + problem.WithExtensions(map[string]any{ |
| 71 | + "currency": "USD", |
| 72 | + "account": "/account/12345", |
| 73 | + }), |
| 74 | +) |
| 75 | +``` |
| 76 | + |
| 77 | +Generates: |
| 78 | + |
| 79 | +```json |
| 80 | +{ |
| 81 | + "type": "about:blank", |
| 82 | + "title": "Unprocessable Entity", |
| 83 | + "status": 422, |
| 84 | + "detail": "You do not have enough credit.", |
| 85 | + "balance": 30, |
| 86 | + "currency": "USD", |
| 87 | + "account": "/account/12345" |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +## Creating Custom Problem Types |
| 92 | + |
| 93 | +You are encouraged to define your own problem types for domain-specific errors. |
| 94 | + |
| 95 | +```go |
| 96 | +var OutOfCredit = problem.TypeTemplate{ |
| 97 | + Type: "https://example.com/probs/out-of-credit", |
| 98 | + Title: "You do not have enough credit.", |
| 99 | + Status: http.StatusForbidden, |
| 100 | +} |
| 101 | + |
| 102 | +// Later in a handler: |
| 103 | +p := problem.New(OutOfCredit, problem.WithDetail("Current balance is 30, but that costs 50.")) |
| 104 | +``` |
| 105 | + |
| 106 | +## Error Wrapping & Unwrapping |
| 107 | + |
| 108 | +Because `*problem.Problem` implements the `error` interface, it can be seamlessly passed through your service layer and wrapped. |
| 109 | + |
| 110 | +```go |
| 111 | +// In your repository/service layer |
| 112 | +func queryDB() error { |
| 113 | + err := db.Query(...) // let's imagine this fails |
| 114 | + |
| 115 | + // Wrap the internal error inside a Problem |
| 116 | + return problem.Wrap(err, problem.InternalServerError, |
| 117 | + problem.WithDetail("Database timeout"), |
| 118 | + problem.WithExtension("trace_id", "req-789"), |
| 119 | + ) |
| 120 | +} |
| 121 | + |
| 122 | +// In your HTTP handler |
| 123 | +func handler(w http.ResponseWriter, r *http.Request) { |
| 124 | + err := queryDB() |
| 125 | + if err != nil { |
| 126 | + // Use errors.As or the provided IsProblem helper to extract it |
| 127 | + if p, ok := problem.IsProblem(err); ok { |
| 128 | + p.Write(w) |
| 129 | + return |
| 130 | + } |
| 131 | + |
| 132 | + // Fallback for unknown errors |
| 133 | + problem.New(problem.InternalServerError).Write(w) |
| 134 | + } |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +## Example Application |
| 139 | + |
| 140 | +Check out the [example/kanban](./example/kanban) directory for a complete, runnable example of using the `problem` library. |
| 141 | +It features: |
| 142 | +- **Clean Architecture**: Clear separation of domain rules, repositories, and HTTP handlers. |
| 143 | +- **Clear Error Boundaries**: Mapping standard domain errors to RFC 7807 problem details. |
| 144 | +- **Go Chi & Govalidator**: Demonstrates using the library with widely used community packages, mapping validation errors to `422 Unprocessable Entity` problem extensions. |
| 145 | + |
0 commit comments