httperr

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Feb 18, 2020 License: BSD-2-Clause Imports: 12 Imported by: 57

README

httperr

GoDoc

Build Status

Package httperr provides utilities for handling error conditions in http clients and servers.

Client

This package provides an http.Client that returns errors for requests that return a status code >= 400. It lets you turn code like this:

func GetFoo() {
    req, _ := http.NewRequest("GET", "https://api.example.com/foo", nil)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    if resp.StatusCode >= 400 {
        return nil, fmt.Errorf("api call failed: %d", resp.StatusCode)
    }
    // ....
}

Into code like this:

func GetFoo() {
    req, _ := http.NewRequest("GET", "https://api.example.com/foo", nil)
    resp, err := httperr.Client().Do(req)
    if err != nil {
        return nil, err
    }
    // ....
}

Wow, three whole lines. Life changing, eh? But wait, there's more!

You can have the client parse structured errors returned from an API:


type APIError struct {
    Message string `json:"message"`
    Code string `json:"code"`
}

func (a APIError) Error() string {
    // APIError must implement the Error interface
    return fmt.Sprintf("%s (code %d)", a.Message, a.Code)
}

func GetFoo() {
    client := httperr.Client(http.DefaultClient, httperr.JSON(APIError{}))

    req, _ := http.NewRequest("GET", "https://api.example.com/foo", nil)
    resp, err := client.Do(req)
    if err != nil {
        // If the server returned a status code >= 400, and the response was valid
        // JSON for APIError, then err is an *APIErr.
        return nil, err
    }
    // ....
}

Server

Error handling in Go's http.Handler and http.HandlerFunc can be tricky. I often found myself wishing that we could just return an err and be done with things.

This package provides an adapter function which turns:

func (s *Server) getUser(w http.ResponseWriter, r *http.Request) {
    remoteUser, err := s.Auth.RequireUser(w, r)
    if err != nil {
        http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
        return
    }

    user, err := s.Storage.Get(remoteUser.Name)
    if err != nil {
        log.Printf("ERROR: cannot fetch user: %s", err)
        http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(user)
}

Into this:

func (s *Server) getUser(w http.ResponseWriter, r *http.Request) error {
    remoteUser, err := s.Auth.RequireUser(w, r)
    if err != nil {
        return httperr.Unauthorized
    }

    user, err := s.Storage.Get(remoteUser.Name)
    if err != nil {
        return err
    }
    return json.NewEncoder(w).Encode(user)
}

Life changing? Probably not, but it seems to remove a lot of redundancy and make control flow in web servers simpler.

You can also wrap your calls with middleware that allow you to provide custom handling of errors that are returned from your handlers, but also >= 400 status codes issued by handlers that don't return errors.

htmlErrorTmpl := template.Must(template.New("err").Parse(errorTemplate))
handler := httperr.Middleware{
    OnError: func(w http.ResponseWriter, r *http.Request, err error) error {
        log.Printf("REQUEST ERROR: %s", err)
        if acceptHeaderContainsTextHTML(r) {
            htmlErrorTmpl.Execute(w, struct{ Error error }{Error: err})
            return nil // nil means we've handled the error
        }
        return err // fall back to the default
    },
    Handler: httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
        if r.Method != "POST" {
            return httperr.MethodNotAllowed
        }
        var reqBody RequestBody
        if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
            return httperr.Public{
                StatusCode: http.StatusBadRequest,
                Err:        err,
            }
        }

        if reqBody.Count <= 0 {
            // The client won't see this, instead OnError will be called with a httperr.Response containing
            // the response. The OnError function can decide to write the error, or replace it with it's own.
            w.WriteHeader(http.StatusConflict)
            fmt.Fprintln(w, "an obscure internal error happened, but the user doesn't want to see this.")
            return nil
        }

        // ...
        return nil
    }),
}

Documentation

Overview

Package httperr implements an error object that speaks HTTP.

Package httperr implements an error object that speaks HTTP.

Index

Constants

This section is empty.

Variables

View Source
var (
	// BadRequest is an error that represents a static http.StatusBadRequest error
	BadRequest = Value{StatusCode: 400}
	// Unauthorized is an error that represents a static http.StatusUnauthorized error
	Unauthorized = Value{StatusCode: 401}
	// PaymentRequired is an error that represents a static http.StatusPaymentRequired error
	PaymentRequired = Value{StatusCode: 402}
	// Forbidden is an error that represents a static http.StatusForbidden error
	Forbidden = Value{StatusCode: 403}
	// NotFound is an error that represents a static http.StatusNotFound error
	NotFound = Value{StatusCode: 404}
	// MethodNotAllowed is an error that represents a static http.StatusMethodNotAllowed error
	MethodNotAllowed = Value{StatusCode: 405}
	// NotAcceptable is an error that represents a static http.StatusNotAcceptable error
	NotAcceptable = Value{StatusCode: 406}
	// ProxyAuthRequired is an error that represents a static http.StatusProxyAuthRequired error
	ProxyAuthRequired = Value{StatusCode: 407}
	// RequestTimeout is an error that represents a static http.StatusRequestTimeout error
	RequestTimeout = Value{StatusCode: 408}
	// Conflict is an error that represents a static http.StatusConflict error
	Conflict = Value{StatusCode: 409}
	// Gone is an error that represents a static http.StatusGone error
	Gone = Value{StatusCode: 410}
	// LengthRequired is an error that represents a static http.StatusLengthRequired error
	LengthRequired = Value{StatusCode: 411}
	// PreconditionFailed is an error that represents a static http.StatusPreconditionFailed error
	PreconditionFailed = Value{StatusCode: 412}
	// RequestEntityTooLarge is an error that represents a static http.StatusRequestEntityTooLarge error
	RequestEntityTooLarge = Value{StatusCode: 413}
	// RequestURITooLong is an error that represents a static http.StatusRequestURITooLong error
	RequestURITooLong = Value{StatusCode: 414}
	// UnsupportedMediaType is an error that represents a static http.StatusUnsupportedMediaType error
	UnsupportedMediaType = Value{StatusCode: 415}
	// RequestedRangeNotSatisfiable is an error that represents a static http.StatusRequestedRangeNotSatisfiable error
	RequestedRangeNotSatisfiable = Value{StatusCode: 416}
	// ExpectationFailed is an error that represents a static http.StatusExpectationFailed error
	ExpectationFailed = Value{StatusCode: 417}
	// Teapot is an error that represents a static http.StatusTeapot error
	Teapot = Value{StatusCode: 418}
	// TooManyRequests is an error that represents a static http.StatusTooManyRequests error
	TooManyRequests = Value{StatusCode: 429}
	// InternalServerError is an error that represents a static http.StatusInternalServerError error
	InternalServerError = Value{StatusCode: 500}
	// NotImplemented is an error that represents a static http.StatusNotImplemented error
	NotImplemented = Value{StatusCode: 501}
	// BadGateway is an error that represents a static http.StatusBadGateway error
	BadGateway = Value{StatusCode: 502}
	// ServiceUnavailable is an error that represents a static http.StatusServiceUnavailable error
	ServiceUnavailable = Value{StatusCode: 503}
	// GatewayTimeout is an error that represents a static http.StatusGatewayTimeout error
	GatewayTimeout = Value{StatusCode: 504}
	// HTTPVersionNotSupported is an error that represents a static http.StatusHTTPVersionNotSupported error
	HTTPVersionNotSupported = Value{StatusCode: 505}
)

Functions

func Client

func Client(next *http.Client, args ...ClientArg) *http.Client

Client returns an http.Client that wraps client with an error handling transport.

func DefaultClient

func DefaultClient() *http.Client

DefaultClient returns an http.Client that wraps the default http.Client with an error handling transport.

func New

func New(statusCode int, err error) error

New returns a new http error wrapping err with status statusCode.

func Public

func Public(statusCode int, err error) error

Public returns a new public http error wrapping err with status statusCode.

func ReportError

func ReportError(r *http.Request, err error)

ReportError reports the error to the function given in OnError.

func StatusCodeAndText

func StatusCodeAndText(err error) (int, string)

StatusCodeAndText returns the status code and text of the error

func Write

func Write(w http.ResponseWriter, r *http.Request, err error)

Write writes the specified error to w. If err is a Writer, then it's WriteError method is invoked to produce the response. Otherwise a generic "500 Internal Server Error" is written.

Types

type ClientArg

type ClientArg func(xport *Transport)

ClientArg is an argument to Client

func JSON

func JSON(errStruct error) ClientArg

JSON returns a ClientArg that specifies a function that handles errors structured as a JSON object.

type HandlerFunc

type HandlerFunc func(http.ResponseWriter, *http.Request) error

The HandlerFunc type is an adapter to allow the use of ordinary functions as HTTP handlers. If f is a function with the appropriate signature, HandlerFunc(f) is a Handler object that calls f.

func (HandlerFunc) ServeHTTP

func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP calls f(w, r).

type Middleware

type Middleware struct {
	// OnError is a function that is called then a request fails with an error. If this function
	// returns nil, then the error is assumed to be handled. If it returns a non-nil error, then
	// that error is written to the client with Write()
	OnError func(w http.ResponseWriter, r *http.Request, err error) error

	// Handler is the next handler
	Handler http.Handler
}

Middleware wraps the provided handler with middleware that captures errors which are returned from HandlerFunc, or reported via ReportError, and invokes the provided callback to render them. If the handler returns a status code >= 400, the response is captured and passed to OnError as a Response.

func (Middleware) ServeHTTP

func (m Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request)

type Response

type Response http.Response

Response is an alias for http.Response that implements the error interface. Example:

resp, err := http.Get("http://www.example.com")
if err != nil {
	return err
}
if resp.StatusCode != http.StatusOK {
	return httperr.Response(*resp)
}
// ...

func (Response) Error

func (re Response) Error() string

func (Response) WriteError

func (re Response) WriteError(w http.ResponseWriter, r *http.Request)

WriteError copies the Response to the ResponseWriter.

type Transport

type Transport struct {
	Next    http.RoundTripper
	OnError func(req *http.Request, resp *http.Response) error
}

Transport is an http.RoundTripper that intercepts responses where the StatusCode >= 400 and returns a Response{}.

If ErrorFactory is specified it should return an error that can be used to unmarshal a JSON error response. This is useful when a web service offers structured error information. If the error structure cannot be unmarshalled, then a regular Response error is returned.

type APIError struct {
  Code string `json:"code"`
  Message string `json:"message"`
}

func (a APIError) Error() string {
   return fmt.Sprintf("%s (%d)", a.Message, a.Code)
}

t := Transport{
    ErrorFactory: func() error {
        return &APIError{}
    },
}

func (Transport) RoundTrip

func (t Transport) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip implements http.RoundTripper.

type Value

type Value struct {
	Err        error  // the underlying error
	StatusCode int    // the HTTP status code. If not supplied, http.StatusInternalServerError is used.
	Status     string // the HTTP status text. If not supplied, http.StatusText(http.StatusCode) is used.
	Public     bool
	Header     http.Header // extra headers to add to the response (optional)
}

Value is an Error that returns that status and code provided, and reveals the underlying wrapper error to the caller. The text of the error is rendered to the client in the body of the response, as well as in the X-Error header.

func (Value) Error

func (e Value) Error() string

func (Value) StatusCodeAndText

func (e Value) StatusCodeAndText() (int, string)

StatusCodeAndText returns the status code and text of the error

func (Value) Unwrap

func (e Value) Unwrap() error

Unwrap unwraps the Value error and returns the underlying error`

func (Value) WriteError

func (e Value) WriteError(w http.ResponseWriter, r *http.Request)

WriteError writes an error response to w using the specified status code.

type Writer

type Writer interface {
	error
	WriteError(w http.ResponseWriter, r *http.Request)
}

Writer is an interface for things that know how to write themselves to an error response. This interface is implemented by Private and Public to provide default error pages.

Jump to

Keyboard shortcuts

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