WARNING: Package hh is experimental while I tinker with the API.
Package hh provides support for HTTP handlers that return errors.
go get github.com/josharian/hh@latest
Full discussion below. But most people just want usage examples, so...
Basic usage. Note that you can mix-and-match wrapped and unwrapped handlers.
mux.HandleFunc("GET /{$}", srv.handleRoot) // normal stdlib handler
mux.HandleFunc("GET /thing/{id}", hh.Wrap(srv.handleThing)) // wrapped handler
Basic handler, showing off lots of different error return options.
func (s *HTTPServer) handleThing(w http.ResponseWriter, r *http.Request) error {
// This is toy code. Don't nitpick or copy unthinkingly, please!
id := r.PathValue("id")
thing, err := s.DB.LookUpThing(id)
if errors.Is(err, sql.ErrNoRows) {
return hh.ErrNotFound // returns a 404, with text "Not Found"
}
if err != nil {
return err // returns a 500, with text "Internal Server Error"
}
authorized := s.checkAuth(r, thing)
if !authorized {
return hh.ErrorText(http.StatusUnauthorized, "no thing for you") // returns 401, with custom text
}
if msg := thing.isBroken(); msg != nil {
return hh.Errorf(http.StatusServiceUnavailable, "this this is temporarily broken: %v", msg)
}
nextAllowedRequest := ratelimitDelay(r)
if delay := nextAllowedRequest.Sub(time.Now()); delay > 0 {
info := map[string]float64{"sleep_sec": delay.Seconds()}
return hh.ErrorJSON(http.StatusTooManyRequests, info) // return a 429 with JSON-encoded info
}
// OK!
json.NewEncoder(w).Write(thing)
return nil
}
Adding in errorware:
func slogHTTP(r *http.Request, err error) error {
if err == nil {
slog.DebugContext(r.Context(), "http request", "method", r.Method, "url", r.URL.String())
} else {
slog.ErrorContext(r.Context(), "http request failed", "method", r.Method, "url", r.URL.String(), "err", err)
}
return err
}
// all handleThing errors (nil or otherwise) will be passed through slogHTTP
mux.HandleFunc("GET /thing/{id}", hh.Wrap(srv.handleThing, slogHTTP)) // all handleThing errors
If this gets repetitive, use a closure:
wrap := func(fn hh.HandlerFunc) http.HandlerFunc {
return hh.Wrap(fn, slogHTTP)
}
The primary goal is to make it easy to write HTTP handlers that return errors. That helps avoid this dreaded mistake:
authorized := checkAuth(r)
if !authorized {
http.Error(w, "unauthorized", http.StatusUnauthorized)
}
// do sensitive stuff
The secondary goals are minimality, compositionality, and just enough "batteries included" APIs for extremely common functionality.
Anything that can be left out! This includes middleware, improved routing, etc.
It is easy to add APIs later, but painful to remove them.
If there's clear consensus on common errorware, they might be a candidate.
There are three parts to the package.
- An adapter for HTTP handlers that return errors (
Wrap
). - Special errors for common HTTP responses.
- Hooks for "errorware": Inspecting, modifying, replacing, or removing errors on the way out.
These are all fundamentally interwoven, which is why they're all lumped together in one package.
The Wrap
adapter converts handlers with errors to handlers. It buffers all responses written by wrapped handlers. This ensures that returning an error at any point is safe to do, because no output will have been written to the client. If buffering is undesirable for a particular endpoint, do not use hh for that endpoint.
The core special error interface is HTTPResponseError
, which gives total control over the HTTP response. There is also a basic implementation, ResponseError
, which supports sending a particular HTTP status code and text. (For more control over responses, such as setting Content-Type headers, create your own implementation to suit your needs.)
There are helpers for the most common uses, implemented using ResponseError
:
Error
responds with the default text for the error code.ErrorText
responds with fixed text and an error code.Errorf
responds with fmt.Sprintf-formatted text.ErrorJSON
responds with JSON-encoded information.
And a set of top level Err*
errors for the most common errors (as determined by some highly scientific grepping).
Wrap
supports integrated errorware, which is a way to log, inspect, and replace errors after the HTTP handler has finished processing.
MIT
Contributions are welcome, but I prioritize my life over open source maintainership. Plan accordingly.
Thank you to Jonathan Hall, Joe Tsai, and David Crawshaw for excellent API design feedback.