summaryrefslogtreecommitdiff
path: root/modules/caddyhttp
diff options
context:
space:
mode:
authorMatthew Holt <mholt@users.noreply.github.com>2019-04-11 20:42:55 -0600
committerMatthew Holt <mholt@users.noreply.github.com>2019-04-11 20:42:55 -0600
commit545f28008e0175491af030f8689cab2112fda9ed (patch)
treedc0d769486b8e05da7c67219292e9c0f95bb7f0b /modules/caddyhttp
parentd42529348fac36f256d4e180ba1c2049f65646c1 (diff)
Begin implementing error handling and re-handling
Diffstat (limited to 'modules/caddyhttp')
-rw-r--r--modules/caddyhttp/caddyhttp.go135
-rw-r--r--modules/caddyhttp/caddylog/log.go12
-rw-r--r--modules/caddyhttp/errors.go105
-rw-r--r--modules/caddyhttp/matchers.go1
-rw-r--r--modules/caddyhttp/routes.go106
5 files changed, 279 insertions, 80 deletions
diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go
index 179ad50..5f1587d 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
+// 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")
}
- 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
+ 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 f7bc9fd..dc940b3 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 0000000..66cb2ca
--- /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 ab179d8..21cc19f 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 0000000..95b6ee8
--- /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
+}