Skip to content

FollowTheProcess/problem

problem

License Go Reference Go Report Card GitHub CI codecov

RFC-7807 Problem JSON structure in Go

Project Description

problem is a single source of truth for an RFC-7807 application/problem+json structure, use it whenever you need to return an error from a REST API and your APIs will all be consistent 👌🏻

Installation

Warning

This module requires the Go 1.25 jsonv2 experiment: GOEXPERIMENT=jsonv2 in order to inline the Extra map when serializing See https://go.dev/blog/jsonv2-exp#experimenting-with-jsonv2

go get go.followtheprocess.codes/problem@latest

Quickstart

Whenever you need a problem:

prob := problem.Problem{
    Type:     "https://example.com/probs/out-of-credit",
    Title:    "Not enough credit",
    Detail:   "Your current balance is 30, but that costs 50",
    Instance: "/account/12345/msgs/abc",
    Status:   http.StatusBadRequest,
}

Or you can use the New function with a bunch of options if you like:

prob := problem.New(
    problem.Type("https://example.com/probs/out-of-credit"),
    problem.Title("Not enough credit"),
    problem.Detail("Your current balance is 30, but that costs 50"),
    problem.Instance("/account/12345/msgs/abc"),
    problem.Status(http.StatusBadRequest),
)

And these will both serialize to the following JSON:

{
  "type": "https://example.com/probs/out-of-credit",
  "title": "Not enough credit",
  "detail": "Your current balance is 30, but that costs 50",
  "instance": "/account/12345/msgs/abc",
  "status": 400
}

Any Extras?

RFC-7807 allows for arbitrary additional fields in this response to convey any additional context your error might have. Using problem you do this with the Extra map:

prob := problem.Problem{
    Type:     "https://example.com/probs/out-of-credit",
    Title:    "Not enough credit",
    Detail:   "Your current balance is 30, but that costs 50",
    Instance: "/account/12345/msgs/abc",
    Status:   http.StatusBadRequest,
    Extra: map[string]any{
      "balance":  30,
      "accounts": []string{"/accounts/12345", "/accounts/67890"},
    },
}

Or with the New pattern:

prob := problem.New(
    problem.Type("https://example.com/probs/out-of-credit"),
    problem.Title("Not enough credit"),
    problem.Detail("Your current balance is 30, but that costs 50"),
    problem.Instance("/account/12345/msgs/abc"),
    problem.Status(http.StatusBadRequest),
    problem.Extra("balance", 30),
    problem.Extra("accounts", []string{"/accounts/12345", "/accounts/67890"}),
)

These will both serialize to the following JSON:

{
  "type": "https://example.com/probs/out-of-credit",
  "title": "Not enough credit",
  "detail": "Your current balance is 30, but that costs 50",
  "instance": "/account/12345/msgs/abc",
  "status": 400,
  "balance": 30,
  "accounts": [
    "/accounts/12345",
    "/accounts/67890"
  ]
}

Tip

There is also an ExtraMap to allow adding a whole map of extra variables in one go rather than one at a time as shown above

In HTTP Handlers

The package also provides some helpers for use in HTTP services to quickly respond with a problem:

package main

import (
	"net/http"

	"go.followtheprocess.codes/problem"
)

func Bang(w http.ResponseWriter, r *http.Request) {
	problem.Respond(
		w,
		problem.Title("Uh oh"),
		problem.Detail("A thing went wrong"),
		problem.Status(http.StatusBadRequest),
	)
}

func main() {
	http.HandleFunc("/", Bang)
	http.ListenAndServe(":8080", nil)
}

Respond will:

  • Set the Status code on the response
  • Write the Content-Type header as application/problem+json
  • Marshal the Problem as JSON to the http.ResponseWriter
    • If that fails, a default problem is written instead

Credits

This package was created with copier and the FollowTheProcess/go-template project template.

About

RFC-7807 Problem JSON in Go

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages