d2/lib/xhttp/err.go
Alexander Wang 524c089a74 oss
Co-authored-by: Anmol Sethi <hi@nhooyr.io>
2022-11-03 06:54:49 -07:00

165 lines
4.2 KiB
Go

package xhttp
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"oss.terrastruct.com/cmdlog"
)
// Error represents an HTTP error.
// It's exported only for comparison in tests.
type Error struct {
Code int
Resp interface{}
Err error
}
var _ interface {
Is(error) bool
Unwrap() error
} = Error{}
// Errorf creates a new error with code, resp, msg and v.
//
// When returned from an xhttp.HandlerFunc, it will be correctly logged
// and written to the connection. See xhttp.WrapHandlerFunc
func Errorf(code int, resp interface{}, msg string, v ...interface{}) error {
return errorWrap(code, resp, fmt.Errorf(msg, v...))
}
// ErrorWrap wraps err with the code and resp for xhttp.HandlerFunc.
//
// When returned from an xhttp.HandlerFunc, it will be correctly logged
// and written to the connection. See xhttp.WrapHandlerFunc
func ErrorWrap(code int, resp interface{}, err error) error {
return errorWrap(code, resp, err)
}
func errorWrap(code int, resp interface{}, err error) error {
if resp == nil {
resp = http.StatusText(code)
}
return Error{code, resp, err}
}
func (e Error) Unwrap() error {
return e.Err
}
func (e Error) Is(err error) bool {
e2, ok := err.(Error)
if !ok {
return false
}
return e.Code == e2.Code && e.Resp == e2.Resp && errors.Is(e.Err, e2.Err)
}
func (e Error) Error() string {
return fmt.Sprintf("http error with code %v and resp %#v: %v", e.Code, e.Resp, e.Err)
}
// HandlerFunc is like http.HandlerFunc but returns an error.
// See Errorf and ErrorWrap.
type HandlerFunc func(w http.ResponseWriter, r *http.Request) error
type HandlerFuncAdapter struct {
Log *cmdlog.Logger
Func HandlerFunc
}
// ServeHTTP adapts xhttp.HandlerFunc into http.Handler for usage with standard
// HTTP routers like chi.
//
// It logs and writes any error from xhttp.HandlerFunc to the connection.
//
// If err was created with xhttp.Errorf or wrapped with xhttp.WrapError, then the error
// will be logged at the correct level for the status code and xhttp.JSON will be called
// with the code and resp.
//
// 400s are logged as warns and 500s as errors.
//
// If the error was not created with the xhttp helpers then a 500 will be written.
//
// If resp is nil, then resp is set to http.StatusText(code)
//
// If the code is not a 400 or a 500, then an error about about the unexpected error code
// will be logged and a 500 will be written. The original error will also be logged.
func (a HandlerFuncAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := a.Func(w, r)
if err != nil {
handleError(a.Log, w, err)
}
})
h.ServeHTTP(w, r)
}
func handleError(clog *cmdlog.Logger, w http.ResponseWriter, err error) {
var herr Error
ok := errors.As(err, &herr)
if !ok {
herr = ErrorWrap(http.StatusInternalServerError, nil, err).(Error)
}
var logger *log.Logger
switch {
case 400 <= herr.Code && herr.Code < 500:
logger = clog.Warn
case 500 <= herr.Code && herr.Code < 600:
logger = clog.Error
default:
logger = clog.Error
clog.Error.Printf("unexpected non error http status code %d with resp: %#v", herr.Code, herr.Resp)
herr.Code = http.StatusInternalServerError
herr.Resp = nil
}
if herr.Resp == nil {
herr.Resp = http.StatusText(herr.Code)
}
logger.Printf("error handling http request: %v", err)
ww, ok := w.(writtenResponseWriter)
if !ok {
clog.Warn.Printf("response writer does not implement Written, double write logs possible: %#v", w)
} else if ww.Written() {
// Avoid double writes if an error occurred while the response was
// being written.
return
}
JSON(clog, w, herr.Code, map[string]interface{}{
"error": herr.Resp,
})
}
type writtenResponseWriter interface {
Written() bool
}
func JSON(clog *cmdlog.Logger, w http.ResponseWriter, code int, v interface{}) {
if v == nil {
v = map[string]interface{}{
"status": http.StatusText(code),
}
}
b, err := json.Marshal(v)
if err != nil {
clog.Error.Printf("json marshal error: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
_, _ = w.Write(b)
}