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