problem

package module
v0.0.0-...-32704d5 Latest Latest
Warning

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

Go to latest
Published: May 22, 2020 License: BSD-3-Clause Imports: 5 Imported by: 1

README

problem

GoDoc

This is an implementation of RFC 7807 for Go.

RFC 7807 attempts to standardize HTTP errors into a machine-readable format which is more detailed than the simple 3-digit HTTP errors. The format is extensible to support more complex error reporting, such as passing back multiple validation errors as a single response. At the core of the standard is the "problem detail" object.

The ProblemDetail object type defined in this code satisfies the Go error interface, which means you can use it as an error return value from function calls. It also supports error wrapping and unwrapping as per Go 1.13 and up.

Why use this?

Frequently a web application Handler/HandlerFunc will have its functionality broken up into multiple sub-functions, any of which could fail for a variety of reasons. For example, you might have something like:

func HandlePut(w http.ResponseWriter, r *http.Request) {
  newrec, err := decodeRequest(r)
  if err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
  }
  currec, err := loadRecord(id)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }
  err := saveRecord(id, newrec)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }
  err := writeChangeLog(currec, newrec)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }
}

It would be nice if a bad request could result in something more specific than 400 Bad Request. There are all sorts of useful HTTP status errors like 404 Not Found (for a PUT to a URI that doesn't correspond to a valid object), 401 Unauthorized if the user's login timed out, 403 Forbidden if they're not allowed to change that particular record, 413 Request Entity Too Large if the PUT request is too big, and so on. Similarly, we might like server-side problems to be more specific than 500 Internal Server Error if possible.

You could have the handler examine the type of the returned error value, assuming the code that created the error defined a custom type, but that will bloat up the handler code quickly.

Another possible approach would be to have the decodeRequest function issue the appropriate HTTP response -- but then you need to make sure every code execution path results in only one HTTP response being issued. If you've ever seen Go warn you http: multiple response.WriteHeader calls, you'll know that spreading http.Error throughout your codebase quickly becomes a problem.

With ProblemDetails, the appropriate HTTP error code, error name and detailed error message for a human can be encapsulated into the returned error, ready for the handler to issue using problem.Write(w, err).

You can mix ProblemDetails error returns with other kinds of error return. A convenience function will report an error including JSON details if it's a ProblemDetails, or construct a default Internal Server Error detail object otherwise:

  err := someFunction()
  if err != nil {
    problem.MustReport(w, err)
    return
  }

Usage

To construct and return errors via the method chaining / fluent API:

  1. Construct an error with problem.New(httpstatus)
  2. Add details using either: a. .Errorf(fmtstr, ...) (like fmt.Errorf) or b. .WithDetail / .WithErr
  3. Return error, or write using .Write

Or, use the non-fluent shortcut methods:

problem.Errorf(httpstatus, fmtstr, ...) 
// like fmt.Errorf but with an HTTP status code

problem.Error(w, msg, status)
// like http.Error

To handle errors:

if err := problem.Report(w, err); err != nil {
  // err wasn't a problem details object, deal with it as you like here
}

or:

problem.MustReport(w, err)
// uses StatusInternalError if err isn't a problem details object

Example

Suppose I have a decodeRequest method which starts like this:

func decodeRequest(r *http.Request) (int64, error) {
	id := chi.URLParam(r, "id")
	nid, err := strconv.ParseInt(id, 10, 64)
	if err != nil {
		return 0, problem.New(http.StatusBadRequest).Errorf("can't parse ID: %w", err)
	}
...

This is called by a handler:

func GetLocation(w http.ResponseWriter, r *http.Request) {
	id, err := rest.decodeRequest(r)
	if err != nil {
		problem.MustWrite(w, err)
		return
	}
...

A GET with an invalid ID then results in this API response:

HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{
  "status": 400,
  "title": "Bad Request",
  "detail": "can't parse ID: strconv.ParseInt: parsing \"che3\": invalid syntax",
  "type": "https://httpstatuses.com/400"
}

Documentation

Index

Constants

View Source
const ContentProblemDetails = "application/problem+json"

ContentProblemDetails is the correct MIME type to use when returning a problem details object as JSON.

Variables

This section is empty.

Functions

func Error

func Error(w http.ResponseWriter, msg string, status int) error

Error is used just like http.Error to create and immediately issue an error.

func MustWrite

func MustWrite(w http.ResponseWriter, err error) error

MustWrite is like Write, but if the error isn't a ProblemDetails object the error is written as a new problem details object, HTTP Internal Server Error.

func Write

func Write(w http.ResponseWriter, err error) error

Write writes the supplied error if it's a ProblemDetails, returning nil; otherwise it returns the error untouched for the caller to handle.

Types

type HTTPError

type HTTPError interface {
	GetStatus() int
}

HTTPError is the minimal interface needed to be able to Write a problem, defined so that ProblemDetails can be encapsulated and expanded as needed.

type ProblemDetails

type ProblemDetails struct {
	Status   int    `json:"status,omitempty"`
	Title    string `json:"title,omitempty"`
	Detail   string `json:"detail,omitempty"`
	Type     string `json:"type,omitempty"`
	Instance string `json:"instance,omitempty"`
	// contains filtered or unexported fields
}

ProblemDetails provides a standard encapsulation for problems encountered in web applications and REST APIs.

func Errorf

func Errorf(status int, fmtstr string, args ...interface{}) *ProblemDetails

Errorf is used like fmt.Errorf to create and return errors. It takes an extra first argument of the HTTP status to use.

func New

func New(status int) *ProblemDetails

New returns a ProblemDetails error object with the given HTTP status code.

func (ProblemDetails) Error

func (pd ProblemDetails) Error() string

New implements the error interface, so ProblemDetails objects can be used as regular error return values.

func (*ProblemDetails) Errorf

func (pd *ProblemDetails) Errorf(fmtstr string, args ...interface{}) *ProblemDetails

Errorf uses fmt.Errorf to add a detail message to the ProblemDetails object. It supports the %w verb.

func (ProblemDetails) GetStatus

func (pd ProblemDetails) GetStatus() int

GetStatus implements the HTTPError interface

func (ProblemDetails) Unwrap

func (pd ProblemDetails) Unwrap() error

Unwrap implements the Go 1.13+ unwrapping interface for errors.

func (*ProblemDetails) WithDetail

func (pd *ProblemDetails) WithDetail(msg string) *ProblemDetails

WithDetail adds the supplied detail message to the problem details.

func (*ProblemDetails) WithErr

func (pd *ProblemDetails) WithErr(err error) *ProblemDetails

WithErr adds an error value as a wrapped error. If the error detail message is currently blank, it is initialized from the error's New() message.

func (*ProblemDetails) Write

func (pd *ProblemDetails) Write(w http.ResponseWriter) error

Write sets the HTTP response code from the ProblemDetails and then sends the entire object as JSON.

type ValidationError

type ValidationError struct {
	FieldName string `json:"name"`
	Error     string `json:"reason"`
}

ValidationError indicates a server-side validation error for data submitted as JSON or via a web form.

type ValidationProblem

type ValidationProblem struct {
	ProblemDetails
	ValidationErrors []ValidationError `json:"invalid-params,omitempty"`
}

ValidationProblem is an example of extending the ProblemDetails structure as per the form validation example in section 3 of RFC 7807, to support reporting of server-side data validation errors.

func NewValidationProblem

func NewValidationProblem() *ValidationProblem

NewValidationProblem creates an object to represent a server-side validation error.

func (*ValidationProblem) Add

func (vp *ValidationProblem) Add(field string, errmsg string)

Add adds a validation error message for the specified field to the ValidationProblem.

Jump to

Keyboard shortcuts

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