Categorygithub.com/crewjam/httperr
modulepackage
0.2.0
Repository: https://github.com/crewjam/httperr.git
Documentation: pkg.go.dev

# 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
    }),
}

# Functions

Client returns an http.Client that wraps client with an error handling transport.
DefaultClient returns an http.Client that wraps the default http.Client with an error handling transport.
JSON returns a ClientArg that specifies a function that handles errors structured as a JSON object.
New returns a new http error wrapping err with status statusCode.
Public returns a new public http error wrapping err with status statusCode.
ReportError reports the error to the function given in OnError.
StatusCodeAndText returns the status code and text of the error.
Write writes the specified error to w.

# Variables

BadGateway is an error that represents a static http.StatusBadGateway error.
BadRequest is an error that represents a static http.StatusBadRequest error.
Conflict is an error that represents a static http.StatusConflict error.
ExpectationFailed is an error that represents a static http.StatusExpectationFailed error.
Forbidden is an error that represents a static http.StatusForbidden error.
GatewayTimeout is an error that represents a static http.StatusGatewayTimeout error.
Gone is an error that represents a static http.StatusGone error.
HTTPVersionNotSupported is an error that represents a static http.StatusHTTPVersionNotSupported error.
InternalServerError is an error that represents a static http.StatusInternalServerError error.
LengthRequired is an error that represents a static http.StatusLengthRequired error.
MethodNotAllowed is an error that represents a static http.StatusMethodNotAllowed error.
NotAcceptable is an error that represents a static http.StatusNotAcceptable error.
NotFound is an error that represents a static http.StatusNotFound error.
NotImplemented is an error that represents a static http.StatusNotImplemented error.
PaymentRequired is an error that represents a static http.StatusPaymentRequired error.
PreconditionFailed is an error that represents a static http.StatusPreconditionFailed error.
ProxyAuthRequired is an error that represents a static http.StatusProxyAuthRequired error.
RequestedRangeNotSatisfiable is an error that represents a static http.StatusRequestedRangeNotSatisfiable error.
RequestEntityTooLarge is an error that represents a static http.StatusRequestEntityTooLarge error.
RequestTimeout is an error that represents a static http.StatusRequestTimeout error.
RequestURITooLong is an error that represents a static http.StatusRequestURITooLong error.
ServiceUnavailable is an error that represents a static http.StatusServiceUnavailable error.
Teapot is an error that represents a static http.StatusTeapot error.
TooManyRequests is an error that represents a static http.StatusTooManyRequests error.
Unauthorized is an error that represents a static http.StatusUnauthorized error.
UnsupportedMediaType is an error that represents a static http.StatusUnsupportedMediaType error.

# Structs

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.
Transport is an http.RoundTripper that intercepts responses where the StatusCode >= 400 and returns a Response{}.
Value is an Error that returns that status and code provided, and reveals the underlying wrapper error to the caller.

# Interfaces

Writer is an interface for things that know how to write themselves to an error response.

# Type aliases

ClientArg is an argument to Client.
The HandlerFunc type is an adapter to allow the use of ordinary functions as HTTP handlers.
Response is an alias for http.Response that implements the error interface.