From 545f28008e0175491af030f8689cab2112fda9ed Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 11 Apr 2019 20:42:55 -0600 Subject: [PATCH] Begin implementing error handling and re-handling --- caddy.go | 6 +- modules/caddyhttp/caddyhttp.go | 137 ++++++++++++------------------ modules/caddyhttp/caddylog/log.go | 12 +++ modules/caddyhttp/errors.go | 105 +++++++++++++++++++++++ modules/caddyhttp/matchers.go | 1 + modules/caddyhttp/routes.go | 106 +++++++++++++++++++++++ 6 files changed, 282 insertions(+), 85 deletions(-) create mode 100644 modules/caddyhttp/errors.go create mode 100644 modules/caddyhttp/routes.go diff --git a/caddy.go b/caddy.go index 1e9829e2..36c9239f 100644 --- a/caddy.go +++ b/caddy.go @@ -161,10 +161,8 @@ func (d *Duration) UnmarshalJSON(b []byte) error { return nil } -// MarshalJSON satisfies json.Marshaler. -func (d Duration) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf(`"%s"`, time.Duration(d).String())), nil -} +// CtxKey is a value type for use with context.WithValue. +type CtxKey string // currentCfg is the currently-loaded configuration. var ( diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go index 179ad50a..5f1587df 100644 --- a/modules/caddyhttp/caddyhttp.go +++ b/modules/caddyhttp/caddyhttp.go @@ -2,9 +2,9 @@ package caddyhttp import ( "context" - "encoding/json" "fmt" "log" + mathrand "math/rand" "net" "net/http" "strconv" @@ -22,6 +22,8 @@ func init() { if err != nil { log.Fatal(err) } + + mathrand.Seed(time.Now().UnixNano()) } type httpModuleConfig struct { @@ -32,36 +34,14 @@ type httpModuleConfig struct { func (hc *httpModuleConfig) Run() error { // TODO: Either prevent overlapping listeners on different servers, or combine them into one - // TODO: A way to loop requests back through, so have them start the matching over again, but keeping any mutations for _, srv := range hc.Servers { - // set up the routes - for i, route := range srv.Routes { - // matchers - for modName, rawMsg := range route.Matchers { - val, err := caddy2.LoadModule("http.matchers."+modName, rawMsg) - if err != nil { - return fmt.Errorf("loading matcher module '%s': %v", modName, err) - } - srv.Routes[i].matchers = append(srv.Routes[i].matchers, val.(RouteMatcher)) - } - - // middleware - for j, rawMsg := range route.Apply { - mid, err := caddy2.LoadModuleInlineName("http.middleware", rawMsg) - if err != nil { - return fmt.Errorf("loading middleware module in position %d: %v", j, err) - } - srv.Routes[i].middleware = append(srv.Routes[i].middleware, mid.(MiddlewareHandler)) - } - - // responder - if route.Respond != nil { - resp, err := caddy2.LoadModuleInlineName("http.responders", route.Respond) - if err != nil { - return fmt.Errorf("loading responder module: %v", err) - } - srv.Routes[i].responder = resp.(Handler) - } + err := srv.Routes.setup() + if err != nil { + return fmt.Errorf("setting up server routes: %v", err) + } + err = srv.Errors.Routes.setup() + if err != nil { + return fmt.Errorf("setting up server error handling routes: %v", err) } s := &http.Server{ @@ -104,65 +84,56 @@ type httpServerConfig struct { ReadTimeout caddy2.Duration `json:"read_timeout"` ReadHeaderTimeout caddy2.Duration `json:"read_header_timeout"` HiddenFiles []string `json:"hidden_files"` // TODO:... experimenting with shared/common state - Routes []serverRoute `json:"routes"` + Routes routeList `json:"routes"` + Errors httpErrorConfig `json:"errors"` } -func (s httpServerConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) { - var mid []Middleware // TODO: see about using make() for performance reasons - var responder Handler - mrw := &middlewareResponseWriter{ResponseWriterWrapper: &ResponseWriterWrapper{w}} +type httpErrorConfig struct { + Routes routeList `json:"routes"` + // TODO: some way to configure the logging of errors, probably? standardize the logging configuration first. +} - for _, route := range s.Routes { - matched := len(route.matchers) == 0 - for _, m := range route.matchers { - if m.Match(r) { - matched = true - break +// ServeHTTP is the entry point for all HTTP requests. +func (s httpServerConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) { + stack := s.Routes.buildMiddlewareChain(w, r) + err := executeMiddlewareChain(w, r, stack) + if err != nil { + // add the error value to the request context so + // it can be accessed by error handlers + c := context.WithValue(r.Context(), ErrorCtxKey, err) + r = r.WithContext(c) + + if len(s.Errors.Routes) == 0 { + // TODO: implement a default error handler? + log.Printf("[ERROR] %s", err) + } else { + errStack := s.Errors.Routes.buildMiddlewareChain(w, r) + err := executeMiddlewareChain(w, r, errStack) + if err != nil { + // TODO: what should we do if the error handler has an error? + log.Printf("[ERROR] handling error: %v", err) } } - if !matched { - continue - } - for _, m := range route.middleware { - mid = append(mid, func(next HandlerFunc) HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) error { - return m.ServeHTTP(mrw, r, next) - } - }) - } - if responder == nil { - responder = route.responder - } - } - - // build the middleware stack, with the responder at the end - stack := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { - if responder == nil { - return nil - } - mrw.allowWrites = true - return responder.ServeHTTP(w, r) - }) - for i := len(mid) - 1; i >= 0; i-- { - stack = mid[i](stack) - } - - err := stack.ServeHTTP(w, r) - if err != nil { - // TODO: error handling - log.Printf("[ERROR] TODO: error handling: %v", err) } } -type serverRoute struct { - Matchers map[string]json.RawMessage `json:"match"` - Apply []json.RawMessage `json:"apply"` - Respond json.RawMessage `json:"respond"` - - // decoded values - matchers []RouteMatcher - middleware []MiddlewareHandler - responder Handler +// executeMiddlewareChain executes stack with w and r. This function handles +// the special ErrRehandle error value, which reprocesses requests through +// the stack again. Any error value returned from this function would be an +// actual error that needs to be handled. +func executeMiddlewareChain(w http.ResponseWriter, r *http.Request, stack Handler) error { + const maxRehandles = 3 + var err error + for i := 0; i < maxRehandles; i++ { + err = stack.ServeHTTP(w, r) + if err != ErrRehandle { + break + } + if i == maxRehandles-1 { + return fmt.Errorf("too many rehandles") + } + } + return err } // RouteMatcher is a type that can match to a request. @@ -206,6 +177,10 @@ func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) error { return f(w, r) } +// emptyHandler is used as a no-op handler, which is +// sometimes better than a nil Handler pointer. +var emptyHandler HandlerFunc = func(w http.ResponseWriter, r *http.Request) error { return nil } + func parseListenAddr(a string) (network string, addrs []string, err error) { network = "tcp" if idx := strings.Index(a, "/"); idx >= 0 { diff --git a/modules/caddyhttp/caddylog/log.go b/modules/caddyhttp/caddylog/log.go index f7bc9fd3..dc940b3c 100644 --- a/modules/caddyhttp/caddylog/log.go +++ b/modules/caddyhttp/caddylog/log.go @@ -13,6 +13,7 @@ func init() { caddy2.RegisterModule(caddy2.Module{ Name: "http.middleware.log", New: func() (interface{}, error) { return new(Log), nil }, + // TODO: Examples of OnLoad and OnUnload. OnLoad: func(instances []interface{}, priorState interface{}) (interface{}, error) { var counter int if priorState != nil { @@ -42,6 +43,17 @@ type Log struct { func (l *Log) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { start := time.Now() + // TODO: An example of returning errors + // return caddyhttp.Error(http.StatusBadRequest, fmt.Errorf("this is a basic error")) + // return caddyhttp.Error(http.StatusBadGateway, caddyhttp.HandlerError{ + // Err: fmt.Errorf("this is a detailed error"), + // Message: "We had trouble doing the thing.", + // Recommendations: []string{ + // "Try reconnecting the gizbop.", + // "Turn off the Internet.", + // }, + // }) + if err := next.ServeHTTP(w, r); err != nil { return err } diff --git a/modules/caddyhttp/errors.go b/modules/caddyhttp/errors.go new file mode 100644 index 00000000..66cb2ca3 --- /dev/null +++ b/modules/caddyhttp/errors.go @@ -0,0 +1,105 @@ +package caddyhttp + +import ( + "fmt" + mathrand "math/rand" + "path" + "runtime" + "strings" + + "bitbucket.org/lightcodelabs/caddy2" +) + +// Error is a convenient way for a Handler to populate the +// essential fields of a HandlerError. If err is itself a +// HandlerError, then any essential fields that are not +// set will be populated. +func Error(statusCode int, err error) HandlerError { + const idLen = 9 + if he, ok := err.(HandlerError); ok { + if he.ID == "" { + he.ID = randString(idLen, true) + } + if he.Trace == "" { + he.Trace = trace() + } + if he.StatusCode == 0 { + he.StatusCode = statusCode + } + return he + } + return HandlerError{ + ID: randString(idLen, true), + StatusCode: statusCode, + Err: err, + Trace: trace(), + } +} + +// HandlerError is a serializable representation of +// an error from within an HTTP handler. +type HandlerError struct { + Err error // the original error value and message + StatusCode int // the HTTP status code to associate with this error + Message string // an optional message that can be shown to the user + Recommendations []string // an optional list of things to try to resolve the error + + ID string // generated; for identifying this error in logs + Trace string // produced from call stack +} + +func (e HandlerError) Error() string { + var s string + if e.ID != "" { + s += fmt.Sprintf("{id=%s}", e.ID) + } + if e.Trace != "" { + s += " " + e.Trace + } + if e.StatusCode != 0 { + s += fmt.Sprintf(": HTTP %d", e.StatusCode) + } + if e.Err != nil { + s += ": " + e.Err.Error() + } + return strings.TrimSpace(s) +} + +// randString returns a string of n random characters. +// It is not even remotely secure OR a proper distribution. +// But it's good enough for some things. It excludes certain +// confusing characters like I, l, 1, 0, O, etc. If sameCase +// is true, then uppercase letters are excluded. +func randString(n int, sameCase bool) string { + if n <= 0 { + return "" + } + dict := []byte("abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY23456789") + if sameCase { + dict = []byte("abcdefghijkmnpqrstuvwxyz0123456789") + } + b := make([]byte, n) + for i := range b { + b[i] = dict[mathrand.Int63()%int64(len(dict))] + } + return string(b) +} + +func trace() string { + if pc, file, line, ok := runtime.Caller(2); ok { + filename := path.Base(file) + pkgAndFuncName := path.Base(runtime.FuncForPC(pc).Name()) + return fmt.Sprintf("%s (%s:%d)", pkgAndFuncName, filename, line) + } + return "" +} + +// ErrRehandle is a special error value that Handlers should return +// from their ServeHTTP() method if the request is to be re-processed. +// This error value is a sentinel value that should not be wrapped or +// modified. +var ErrRehandle = fmt.Errorf("rehandling request") + +// ErrorCtxKey is the context key to use when storing +// an error (for use with context.Context). +const ErrorCtxKey = caddy2.CtxKey("handler_chain_error") diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index ab179d85..21cc19f4 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -137,6 +137,7 @@ func (m matchHeader) Match(r *http.Request) bool { return false } +// Interface guards var ( _ RouteMatcher = matchHost{} _ RouteMatcher = matchPath{} diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go new file mode 100644 index 00000000..95b6ee82 --- /dev/null +++ b/modules/caddyhttp/routes.go @@ -0,0 +1,106 @@ +package caddyhttp + +import ( + "encoding/json" + "fmt" + "net/http" + + "bitbucket.org/lightcodelabs/caddy2" +) + +type serverRoute struct { + Matchers map[string]json.RawMessage `json:"match"` + Apply []json.RawMessage `json:"apply"` + Respond json.RawMessage `json:"respond"` + + Exclusive bool `json:"exclusive"` + + // decoded values + matchers []RouteMatcher + middleware []MiddlewareHandler + responder Handler +} + +type routeList []serverRoute + +func (routes routeList) buildMiddlewareChain(w http.ResponseWriter, r *http.Request) Handler { + if len(routes) == 0 { + return emptyHandler + } + + var mid []Middleware + var responder Handler + mrw := &middlewareResponseWriter{ResponseWriterWrapper: &ResponseWriterWrapper{w}} + + for _, route := range routes { + matched := len(route.matchers) == 0 + for _, m := range route.matchers { + if m.Match(r) { + matched = true + break + } + } + if !matched { + continue + } + for _, m := range route.middleware { + mid = append(mid, func(next HandlerFunc) HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) error { + return m.ServeHTTP(mrw, r, next) + } + }) + } + if responder == nil { + responder = route.responder + } + if route.Exclusive { + break + } + } + + // build the middleware stack, with the responder at the end + stack := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + if responder == nil { + return nil + } + mrw.allowWrites = true + return responder.ServeHTTP(w, r) + }) + for i := len(mid) - 1; i >= 0; i-- { + stack = mid[i](stack) + } + + return stack +} + +func (routes routeList) setup() error { + for i, route := range routes { + // matchers + for modName, rawMsg := range route.Matchers { + val, err := caddy2.LoadModule("http.matchers."+modName, rawMsg) + if err != nil { + return fmt.Errorf("loading matcher module '%s': %v", modName, err) + } + routes[i].matchers = append(routes[i].matchers, val.(RouteMatcher)) + } + + // middleware + for j, rawMsg := range route.Apply { + mid, err := caddy2.LoadModuleInlineName("http.middleware", rawMsg) + if err != nil { + return fmt.Errorf("loading middleware module in position %d: %v", j, err) + } + routes[i].middleware = append(routes[i].middleware, mid.(MiddlewareHandler)) + } + + // responder + if route.Respond != nil { + resp, err := caddy2.LoadModuleInlineName("http.responders", route.Respond) + if err != nil { + return fmt.Errorf("loading responder module: %v", err) + } + routes[i].responder = resp.(Handler) + } + } + return nil +}