Skip to content

Instantly share code, notes, and snippets.

@xeoncross
Last active October 2, 2021 19:03
Show Gist options
  • Save xeoncross/c6e20af40e25014011bc92bcb88441ee to your computer and use it in GitHub Desktop.
Save xeoncross/c6e20af40e25014011bc92bcb88441ee to your computer and use it in GitHub Desktop.
Golang - Go http.Handler returning and error that can split user messages from log messages

Demo: https://play.golang.org/p/qk20EKdlRuL

One message (somewhere in the chain of errors) is for the user - the rest are for the logs. The idea is that you can still put plenty of debugging information in the full error logs ("SQL error") - but show a nice, safe message to the client ("user not found").

Based on the following articles (discussed above)

https://middlemost.com/failure-is-your-domain/ https://commandcenter.blogspot.com/2017/12/error-handling-in-upspin.html https://blog.questionable.services/article/http-handler-error-handling-revisited/ https://go.dev/blog/error-handling-and-go

https://www.reddit.com/r/golang/comments/pqpgwp/is_there_a_better_way_to_return_errors_in_http/

Stack traces

The following libraries add stack trace features which is a different problem/feature than tackled in this gist

package main
import (
"errors"
"fmt"
"log"
"net/http"
"net/http/httptest"
"testing"
)
/*
Modify http.HandlerFuncs to return errors for reducing the amount of code required to:
1. separate logging errors from user-facing messages
2. add tracing or context values
3. avoid missing empty returns
- https://go.dev/blog/error-handling-and-go
- http://blog.questionable.services/article/http-handler-error-handling-revisited/
*/
type Error struct {
Err error
Message string
Code int
}
// So errors.Is() still works
func (e Error) Unwrap() error {
return e.Err
}
func (e Error) Error() string {
return e.Err.Error()
}
// ErrorCode returns the code of the highest error, if available. Otherwise returns http.StatusInternalServerError
func ErrorCode(err error) int {
if err == nil {
return http.StatusInternalServerError
} else if e, ok := err.(Error); ok && e.Code != 0 {
return e.Code
} else if ok && e.Err != nil {
return ErrorCode(e.Err)
} else if err2 := errors.Unwrap(err); err2 != nil {
return ErrorCode(err2)
}
return http.StatusInternalServerError
}
// ErrorMessage returns the user message of the root error, if available. Otherwise returns
func ErrorMessage(err error) string {
if err == nil {
return http.StatusText(http.StatusInternalServerError)
} else if e, ok := err.(Error); ok && e.Message != "" {
return e.Message
} else if ok && e.Err != nil {
return ErrorMessage(e.Err)
} else if err2 := errors.Unwrap(err); err2 != nil {
return ErrorMessage(err2)
}
return http.StatusText(http.StatusInternalServerError)
}
// maybe each app can implement this so they can add whatever metrics or logging they want
type appHandler func(http.ResponseWriter, *http.Request) error
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := fn(w, r); err != nil {
// todo: add r.Context trace id to logs
// Report all errors and nested errors
log.Printf("HTTP %d - %s", ErrorCode(err), err)
// Show the client a "safe" error or http.StatusInternalServerError
http.Error(w, ErrorMessage(err), ErrorCode(err))
// Extra: verify we can still check the error type and didn't loose the power of .Is() and .Unwrap()
log.Printf("errors.is = %v\n", errors.Is(err, ErrStartingItAll))
}
}
// Lets create a handler that tries to load the user or something
func myhandler(w http.ResponseWriter, r *http.Request) error {
err := fetchUser()
if err != nil {
return fmt.Errorf("myhandler: %s: %w", r.URL.String(), err)
}
return nil
}
var ErrStartingItAll = errors.New("real error msg")
// the database call fails and we want to show the real error in the log, but the "user safe" error to the client
func fetchUser() error {
return Error{
Err: ErrStartingItAll,
Message: "user error msg",
Code: http.StatusBadRequest,
}
}
func TestHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
mux := http.NewServeMux()
mux.Handle("/", appHandler(myhandler))
mux.ServeHTTP(rr, req)
t.Logf("HTTP: %d %s", rr.Result().StatusCode, rr.Body)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment