problems

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 20, 2025 License: Apache-2.0 Imports: 5 Imported by: 19

README

Problems

Problems is an RFC-7807 and RFC-9457 compliant library for describing HTTP errors. For more information see RFC-9457, and it's predecessor RFC-7807.

Build Status Go Report Card GoDoc

Usage

The problems library exposes an assortment of types to aid HTTP service authors in defining and using HTTP Problem detail resources.

Predefined Errors

You can define basic Problem details up front by using the NewStatusProblem function

package main

import "github.com/moogar0880/problems"

var (
  // The NotFound problem will be built with an appropriate status code and
  // informative title set. Additional information can be provided in the Detail
  // field of the generated struct
  NotFound = problems.NewStatusProblem(404)
)

Which, when served over HTTP as JSON will look like the following:

{
   "type": "about:blank",
   "title": "Not Found",
   "status": 404
}
Detailed Errors

New errors can also be created a head of time, or on the fly like so:

package main

import "github.com/moogar0880/problems"

func NoSuchUser() *problems.Problem {
    nosuch := problems.NewStatusProblem(404)
    nosuch.Detail = "Sorry, that user does not exist."
    return nosuch
}

Which, when served over HTTP as JSON will look like the following:

{
   "type": "about:blank",
   "title": "Not Found",
   "status": 404,
   "detail": "Sorry, that user does not exist."
}
Extended Errors

The specification for these HTTP problems was designed to allow for arbitrary expansion of the problem resources. This can be accomplished through this library by either embedding the Problem struct in your extension type:

package main

import "github.com/moogar0880/problems"

type CreditProblem struct {
    problems.Problem

    Balance  float64  `json:"balance"`
    Accounts []string `json:"accounts"`
}

Which, when served over HTTP as JSON will look like the following:

{
   "type": "about:blank",
   "title": "Unauthorized",
   "status": 401,
   "balance": 30,
   "accounts": ["/account/12345", "/account/67890"]
}

Or by using the problems.ExtendedProblem type:

package main

import (
    "net/http"

    "github.com/moogar0880/problems"
)

type CreditProblemExt struct {
    Balance  float64  `json:"balance"`
    Accounts []string `json:"accounts"`
}

func main() {
    problems.NewExt[CreditProblemExt]().
        WithStatus(http.StatusForbidden).
        WithDetail("Your account does not have sufficient funds to complete this transaction").
        WithExtension(CreditProblemExt{
            Balance:  30,
            Accounts: []string{"/account/12345", "/account/67890"},
        })
}

Which, when served over HTTP as JSON will look like the following:

{
   "type": "about:blank",
   "title": "Unauthorized",
   "status": 401, 
   "extensions": {
       "balance": 30,
       "accounts": ["/account/12345", "/account/67890"]    
   }
}

Serving Problems

Additionally, RFC-7807 defines two new media types for problem resources, application/problem+json" and application/problem+xml. This library defines those media types as the constants ProblemMediaType and ProblemMediaTypeXML.

In order to facilitate serving problem definitions, this library exposes two http.HandlerFunc implementations which accept a problem, and return a functioning HandlerFunc that will server that error.

package main

import (
    "net/http"

    "github.com/moogar0880/problems"
)

var Unauthorized = problems.NewStatusProblem(401)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/secrets", problems.ProblemHandler(Unauthorized))

    server := http.Server{Handler: mux, Addr: ":8080"}
    server.ListenAndServe()
}

Documentation

Overview

Package problems provides an RFC-9457 (https://tools.ietf.org/html/rfc9457) and RFC 7807 (https://tools.ietf.org/html/rfc7807) compliant implementation of HTTP problem details. Which are defined as a means to carry machine-readable details of errors in an HTTP response to avoid the need to define new error response formats for HTTP APIs.

The problem details specification was designed to allow for schema extensions. There are two possible ways to create problem extensions:

1. You can embed a problem in your extension problem type. 2. You can use the ExtendedProblem to leverage the existing types in this library.

See the examples for references on how to use either of these extension mechanisms.

Additionally, this library also ships with default http.HandlerFunc implementations which are capable of writing problems to a http.ResponseWriter in either of the two standard media formats, JSON and XML.

Index

Examples

Constants

View Source
const (
	// ProblemMediaType is the default media type for a Problem response
	ProblemMediaType = "application/problem+json"

	// ProblemMediaTypeXML is the XML variant on the Problem Media type
	ProblemMediaTypeXML = "application/problem+xml"

	// DefaultURL is the default url to use for problem types
	DefaultURL = "about:blank"
)

Variables

View Source
var ErrTitleMustBeSet = fmt.Errorf("%s: problem title must be set", errPrefix)

ErrTitleMustBeSet is the error returned from a call to ValidateProblem if the problem is validated without a title.

Functions

func NewErrInvalidProblemType

func NewErrInvalidProblemType(value string, e error) error

NewErrInvalidProblemType returns a new ErrInvalidProblemType instance which wraps the provided error.

func ProblemHandler

func ProblemHandler(p *Problem) http.HandlerFunc

ProblemHandler returns a http.HandlerFunc which writes a provided problem to a http.ResponseWriter as JSON with the status code.

func XMLProblemHandler

func XMLProblemHandler(p *Problem) http.HandlerFunc

XMLProblemHandler returns a http.HandlerFunc which writes a provided problem to a http.ResponseWriter as XML with the status code.

Types

type ErrInvalidProblemType

type ErrInvalidProblemType struct {
	Err   error
	Value string
}

ErrInvalidProblemType is the error type returned if a problems type is not a valid URI when it is validated. The inner Err will contain the error returned from attempting to parse the invalid URI.

func (*ErrInvalidProblemType) Error

func (e *ErrInvalidProblemType) Error() string

type ExtendedProblem added in v1.0.0

type ExtendedProblem[T any] struct {
	Problem

	// Extensions allows for Problem type definitions to extend the standard
	// problem details object with additional members that are specific to that
	// problem type.
	Extensions T `json:"extensions,omitempty" xml:"extensions,omitempty"`
}

An ExtendedProblem extends the Problem type with a new field, Extensions, of type T.

Example
package main

import (
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/moogar0880/problems"
)

func main() {
	type CreditProblemExt struct {
		Balance  float64  `json:"balance"`
		Accounts []string `json:"accounts"`
	}
	problem := problems.NewExt[CreditProblemExt]().
		WithStatus(http.StatusForbidden).
		WithDetail("You do not have sufficient funds to complete this transaction.").
		WithExtension(CreditProblemExt{
			Balance:  30,
			Accounts: []string{"/account/12345", "/account/67890"},
		})
	b, _ := json.MarshalIndent(problem, "", "  ")
	fmt.Println(string(b))
}
Output:

{
  "type": "about:blank",
  "title": "Forbidden",
  "status": 403,
  "detail": "You do not have sufficient funds to complete this transaction.",
  "extensions": {
    "balance": 30,
    "accounts": [
      "/account/12345",
      "/account/67890"
    ]
  }
}
Example (Embedding)
package main

import (
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/moogar0880/problems"
)

func main() {
	type CreditProblem struct {
		problems.Problem

		Balance  float64  `json:"balance"`
		Accounts []string `json:"accounts"`
	}
	problem := &CreditProblem{
		Problem: *problems.New().
			WithStatus(http.StatusForbidden).
			WithDetail("You do not have sufficient funds to complete this transaction."),
		Balance:  30,
		Accounts: []string{"/account/12345", "/account/67890"},
	}

	b, _ := json.MarshalIndent(problem, "", "  ")
	fmt.Println(string(b))
}
Output:

{
  "type": "about:blank",
  "title": "Forbidden",
  "status": 403,
  "detail": "You do not have sufficient funds to complete this transaction.",
  "balance": 30,
  "accounts": [
    "/account/12345",
    "/account/67890"
  ]
}

func ExtFromError added in v1.0.0

func ExtFromError[T any](err error) *ExtendedProblem[T]

ExtFromError returns a new ExtendedProblem instance which contains the string version of the provided error as the details of the problem.

func Extend added in v1.0.0

func Extend[T any](p *Problem, ext T) *ExtendedProblem[T]

Extend allows you to convert a standard Problem instance to an ExtendedProblem with the provided extension data.

func NewExt added in v1.0.0

func NewExt[T any]() *ExtendedProblem[T]

NewExt returns a new ExtendedProblem with all the same default values as applied by a call to New.

func (*ExtendedProblem[T]) Error added in v1.0.0

func (p *ExtendedProblem[T]) Error() string

Error implements the error interface and allows a Problem to be used as a native error.

func (*ExtendedProblem[T]) Validate added in v1.0.0

func (p *ExtendedProblem[T]) Validate() (*ValidExtendedProblem[T], error)

Validate validates the content of the ExtendedProblem instance. If the ExtendedProblem is invalid, as defined by RFC-9457, then an error explaining the validation error is returned. Otherwise, a sealed ValidProblem instance is returned.

See the documentation for ErrTitleMustBeSet and ErrInvalidProblemType for more information on the validation errors returned by this method.

func (*ExtendedProblem[T]) WithDetail added in v1.0.0

func (p *ExtendedProblem[T]) WithDetail(detail string) *ExtendedProblem[T]

WithDetail sets the detail message to the provided string.

func (*ExtendedProblem[T]) WithDetailf added in v1.0.0

func (p *ExtendedProblem[T]) WithDetailf(format string, args ...interface{}) *ExtendedProblem[T]

WithDetailf behaves identically to WithDetail, but allows consumers to provide a format string and arguments which will be formatted internally.

func (*ExtendedProblem[T]) WithError added in v1.0.0

func (p *ExtendedProblem[T]) WithError(err error) *ExtendedProblem[T]

WithError sets the detail message to the provided error.

func (*ExtendedProblem[T]) WithExtension added in v1.0.0

func (p *ExtendedProblem[T]) WithExtension(ext T) *ExtendedProblem[T]

WithExtension sets the extensions value to the provided extension of type T.

func (*ExtendedProblem[T]) WithInstance added in v1.0.0

func (p *ExtendedProblem[T]) WithInstance(instance string) *ExtendedProblem[T]

WithInstance sets the instance uri to the provided string.

func (*ExtendedProblem[T]) WithStatus added in v1.0.0

func (p *ExtendedProblem[T]) WithStatus(status int) *ExtendedProblem[T]

WithStatus sets the status field to the provided int.

If no title is set then this call will also set the title to the return value of http.StatusText for the provided status code.

func (*ExtendedProblem[T]) WithTitle added in v1.0.0

func (p *ExtendedProblem[T]) WithTitle(title string) *ExtendedProblem[T]

WithTitle sets the title field to the provided string.

func (*ExtendedProblem[T]) WithType added in v1.0.0

func (p *ExtendedProblem[T]) WithType(typ string) *ExtendedProblem[T]

WithType sets the type field to the provided string.

type Problem

type Problem struct {
	// Type contains a URI that identifies the problem type. This URI will,
	// ideally, contain human-readable documentation for the problem when
	// de-referenced.
	Type string `json:"type" xml:"type"`

	// Title is a short, human-readable summary of the problem type. This title
	// SHOULD NOT change from occurrence to occurrence of the problem, except
	// for purposes of localization.
	Title string `json:"title" xml:"title"`

	// The HTTP status code for this occurrence of the problem.
	Status int `json:"status,omitempty" xml:"status,omitempty"`

	// A human-readable explanation specific to this occurrence of the problem.
	Detail string `json:"detail,omitempty" xml:"detail,omitempty"`

	// A URI that identifies the specific occurrence of the problem. This URI
	// may or may not yield further information if de-referenced.
	Instance string `json:"instance,omitempty" xml:"instance,omitempty"`
}

A Problem defines all the standard problem detail fields as defined by RFC-9457 and can easily be serialized to either JSON or XML.

To add extensions to a Problem definition, see ExtendedProblem or consider embedding a Problem in your extension struct.

func FromError added in v1.0.0

func FromError(err error) *Problem

FromError returns a new Problem instance which contains the string version of the provided error as the details of the problem.

Example
package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"

	"github.com/moogar0880/problems"
)

func main() {
	err := func() error {
		// Some fallible function.
		return errors.New("something bad happened")
	}()
	internalServerError := problems.FromError(err).WithStatus(http.StatusInternalServerError)
	b, _ := json.MarshalIndent(internalServerError, "", "  ")
	fmt.Println(string(b))
}
Output:

{
  "type": "about:blank",
  "title": "Internal Server Error",
  "status": 500,
  "detail": "something bad happened"
}

func New added in v1.0.0

func New() *Problem

New returns a new Problem instance with the type field set to DefaultURL.

func NewDetailedProblem

func NewDetailedProblem(status int, details string) *Problem

NewDetailedProblem returns a new Problem with a Detail string set for a more detailed explanation of the problem being returned.

func NewStatusProblem

func NewStatusProblem(status int) *Problem

NewStatusProblem will generate a default problem for the provided HTTP status code. The Problem's Status field will be set to match the status argument, and the Title will be set to the default Go status text for that code.

Example
package main

import (
	"encoding/json"
	"fmt"

	"github.com/moogar0880/problems"
)

func main() {
	notFound := problems.NewStatusProblem(404)
	b, _ := json.MarshalIndent(notFound, "", "  ")
	fmt.Println(string(b))
}
Output:

{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404
}
Example (Detailed)
package main

import (
	"encoding/json"
	"fmt"

	"github.com/moogar0880/problems"
)

func main() {
	notFound := problems.NewStatusProblem(404)
	notFound.Detail = "The item you've requested either does not exist or has been deleted."
	b, _ := json.MarshalIndent(notFound, "", "  ")
	fmt.Println(string(b))
}
Output:

{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "The item you've requested either does not exist or has been deleted."
}

func (*Problem) Error added in v1.0.0

func (p *Problem) Error() string

Error implements the error interface and allows a Problem to be used as a native error.

func (*Problem) Validate added in v1.0.0

func (p *Problem) Validate() (*ValidProblem, error)

Validate validates the content of the Problem instance. If the Problem is invalid, as defined by RFC-9457, then an error explaining the validation error is returned. Otherwise, a sealed ValidProblem instance is returned.

See the documentation for ErrTitleMustBeSet and ErrInvalidProblemType for more information on the validation errors returned by this method.

func (*Problem) WithDetail added in v1.0.0

func (p *Problem) WithDetail(detail string) *Problem

WithDetail sets the detail message to the provided string.

func (*Problem) WithDetailf added in v1.0.0

func (p *Problem) WithDetailf(format string, args ...interface{}) *Problem

WithDetailf behaves identically to WithDetail, but allows consumers to provide a format string and arguments which will be formatted internally.

func (*Problem) WithError added in v1.0.0

func (p *Problem) WithError(err error) *Problem

WithError sets the detail message to the provided error.

func (*Problem) WithInstance added in v1.0.0

func (p *Problem) WithInstance(instance string) *Problem

WithInstance sets the instance uri to the provided string.

func (*Problem) WithStatus added in v1.0.0

func (p *Problem) WithStatus(status int) *Problem

WithStatus sets the status field to the provided int.

If no title is set then this call will also set the title to the return value of http.StatusText for the provided status code.

func (*Problem) WithTitle added in v1.0.0

func (p *Problem) WithTitle(title string) *Problem

WithTitle sets the title field to the provided string.

func (*Problem) WithType added in v1.0.0

func (p *Problem) WithType(typ string) *Problem

WithType sets the type field to the provided string.

type ValidExtendedProblem added in v1.0.0

type ValidExtendedProblem[T any] struct {
	ValidProblem
	// contains filtered or unexported fields
}

A ValidExtendedProblem is a sealed variant of ExtendedProblem which is guaranteed to contain valid fields.

Instances of ValidExtendedProblem can be created by using the Validate method on the ExtendedProblem type.

func (*ValidExtendedProblem[T]) IntoExtendedProblem added in v1.0.0

func (p *ValidExtendedProblem[T]) IntoExtendedProblem() *ExtendedProblem[T]

IntoExtendedProblem allows you to convert from a ValidExtendedProblem back into an ExtendedProblem.

func (*ValidExtendedProblem[T]) IntoProblem added in v1.0.0

func (p *ValidExtendedProblem[T]) IntoProblem() *Problem

IntoProblem allows you to convert from a ValidExtendedProblem back into a Problem.

func (*ValidExtendedProblem[T]) MarshalJSON added in v1.0.0

func (p *ValidExtendedProblem[T]) MarshalJSON() ([]byte, error)

MarshalJSON implements the json.Marshaler interface and ensures that a ValidExtendedProblem is properly serialized into JSON.

type ValidProblem added in v1.0.0

type ValidProblem struct {
	// contains filtered or unexported fields
}

A ValidProblem is a sealed variant of Problem which is guaranteed to have valid fields.

Instances of ValidProblem can be created by using the Validate method from the Problem type.

func (*ValidProblem) IntoProblem added in v1.0.0

func (p *ValidProblem) IntoProblem() *Problem

IntoProblem allows you to convert from a ValidProblem back into a Problem.

func (*ValidProblem) MarshalJSON added in v1.0.0

func (p *ValidProblem) MarshalJSON() ([]byte, error)

MarshalJSON implements the json.Marshaler interface and ensures that a ValidProblem is properly serialized into JSON.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL