https://github.com/kodeart/go-problem
Problem details for HTTP APIs per RFC-9457 standard
https://github.com/kodeart/go-problem
api-problem error go golang problem problem-details rfc-7807 rfc-9457
Last synced: 7 months ago
JSON representation
Problem details for HTTP APIs per RFC-9457 standard
- Host: GitHub
- URL: https://github.com/kodeart/go-problem
- Owner: kodeart
- License: mit
- Created: 2024-08-08T08:21:06.000Z (over 1 year ago)
- Default Branch: master
- Last Pushed: 2025-03-25T10:34:42.000Z (10 months ago)
- Last Synced: 2025-03-25T11:32:07.641Z (10 months ago)
- Topics: api-problem, error, go, golang, problem, problem-details, rfc-7807, rfc-9457
- Language: Go
- Homepage:
- Size: 42 KB
- Stars: 3
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Problem Details
[](https://codecov.io/gh/kodeart/go-problem)
[](https://goreportcard.com/report/github.com/kodeart/go-problem)
[](https://github.com/kodeart/go-problem/blob/master/LICENSE)
## Problem details for HTTP APIs per [RFC-9457][RFC9457] standard.
This module provides a `Problem` struct which can be used to represent a problem
in HTTP APIs. It implements the [RFC-9457][RFC9457] standard that creates error responses.
### The benefits of using `go-problem`
- RFC 9457 and 7807 compliant error responses
- Consistent error format across the API
- Built-in support for HTTP status codes
- Extensible error details
- Clean and readable error handling
- Built-in JSON and XML marshaling
## Installation
```bash
go get github.com/kodeart/go-problem/v2
```
Update
```
go get -u github.com/kodeart/go-problem/v2
go mod tidy
```
### What is it again?
It's for creating a standardized error responses, like this JSON for example:
```json
{
"status": 403,
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
"balance": 30,
"accounts": ["/account/12345", "/account/67890"]
}
```
or
```xml
https://example.com/probs/out-of-credit
You do not have enough credit.
Your current balance is 30, but that costs 50.
https://example.net/account/12345/msgs/abc
30
https://example.net/account/12345
https://example.net/account/67890
```
or even this, **which defeats the whole purpose of the RFC**, but it may be useful for your current/legacy code:
```json
{
"message": "Failed to use the problem details",
"code": 500
}
```
```xml
Failed to use the problem details
500
```
## Usage
`go-problem` module provides an easy way to send the `Problem` struct as a response to the client.
### Example with HTTP handler and middleware
```go
package middleware
import (
"net/http"
"github.com/kodeart/go-problem/v2"
)
func NotFoundHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
p := problem.Problem{
Status: http.StatusNotFound,
Detail: "No such API route",
Title: "Route Not Found",
Instance: r.URL.Path,
}
p.JSON(w)
}
}
// or with helper methods
func NotFoundHandler(w http.ResponseWriter, r *http.Request) {
problem.New().
WithStatus(http.StatusNotFound).
WithDetail("No such API route").
WithTitle("Route Not Found").
WithInstance(r.URL.Path).
JSON(w)
}
```
```go
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/kodeart/go-problem/v2"
)
func main() {
mux := chi.NewRouter()
mux.NotFound(middleware.NotFoundHandler)
// ...
mux.Get("/", func(w http.ResponseWriter, r *http.Request) {
problem.New().
WithStatus(http.StatusServiceUnavailable).
WithExtension("maintenance", true).
WithExtension("version", "1.0.0").
JSON(w)
})
}
```
### Helpers
Any key-value pair outide the standard fields can be accessed with
```go
p := problem.New().WithExtension("key", "value")
v := p.GetExtension("key")
// if you know the type of the value, assert it
intVal := p.GetExtension("key name").(int)
```
If there is no such element, `nil` is returned.
### Create a `Problem` with helpers
```go
p := problem.New().
WithStatus(http.StatusUnprocessableEntity).
WithType("https://example.com/probs/out-of-credit").
WithTitle("You do not have enough credit.").
WithDetail("Your current balance is 30, but that costs 50.").
WithInstance("/account/12345/msgs/abc").
WithExtension("balance", 30).
WithExtension("accounts", []string{
"/account/12345",
"/account/67890",
})
}
```
### Create a `Problem` directly
```go
p := problem.Problem{
Status: http.StatusUnprocessableEntity,
Type: "https://example.com/probs/out-of-credit",
Title: "You do not have enough credit.",
Detail: "Your current balance is 30, but that costs 50.",
Instance: "/account/12345/msgs/abc",
Extensions: map[string]any{
"balance": 30,
"accounts": []string{
"/account/12345",
"/account/67890",
},
},
}
```
## Rendering
`Problem` supports serializing and deserializing the data to and from JSON and XML.
### JSON
The `JSON()` method will render the `Problem` struct to JSON string,
```go
p.JSON(w)
```
while the standard `json.Unmarshal()` method will try to parse a JSON string into a `Problem` struct:
```go
var p problem.Problem
jsonString := `{...}` // some error message
err := json.Unmarshal([]byte(jsonString), &p)
```
### XML
The `XML()` method will render the `Problem` struct to XML string,
```go
p.XML(w)
```
```go
var p problem.Problem
xmlString := `...`
err := xml.Unmarshal([]byte(xmlString), &p)
```
> When unmarshalling the XML into `Problem` struct,
> the unmarshaller will try it's best to create the extensions.
> Expect all values to be of a `string` type.
### Content-Type header
The final response should have a `Content-Type: application/problem+json` or
`Content-Type: application/problem+xml` header:
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
Vary: Origin
Date: Sat, 01 Jan 1970 17:21:18 GMT
Content-Length: 87
{"detail":"No such API route","instance":"/foo","status":404,"title":"Route Not Found"}
### Cache-Control header
The response **will not have** a `Cache-Control: no-cache, no-store, must-revalidate` header
according to the [RFC-7231][RFC7231] for HTTP/1.1, unless otherwise explicitly set.
### Thread-satey considerations
`UnmarshalJSON` and `UnmarshalXML` modifies the receiver, so the concurrent calls
with the same receiver would be unsafe.
> - `Problem` instances should not be modified after creation
> - Each request should use its own `Problem` instance
> - `UnmarshalJSON` and `UnmarshalXML` should not be called concurrently on the same instance
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
[RFC9457]: https://tools.ietf.org/html/rfc9457
[RFC7231]: https://tools.ietf.org/html/rfc7231