165 lines
4.2 KiB
Go
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)
|
|
}
|