Begin implementing error handling and re-handling

This commit is contained in:
Matthew Holt 2019-04-11 20:42:55 -06:00
parent d42529348f
commit 545f28008e
6 changed files with 282 additions and 85 deletions

View File

@ -161,10 +161,8 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// MarshalJSON satisfies json.Marshaler. // CtxKey is a value type for use with context.WithValue.
func (d Duration) MarshalJSON() ([]byte, error) { type CtxKey string
return []byte(fmt.Sprintf(`"%s"`, time.Duration(d).String())), nil
}
// currentCfg is the currently-loaded configuration. // currentCfg is the currently-loaded configuration.
var ( var (

View File

@ -2,9 +2,9 @@ package caddyhttp
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"log" "log"
mathrand "math/rand"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
@ -22,6 +22,8 @@ func init() {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
mathrand.Seed(time.Now().UnixNano())
} }
type httpModuleConfig struct { type httpModuleConfig struct {
@ -32,36 +34,14 @@ type httpModuleConfig struct {
func (hc *httpModuleConfig) Run() error { func (hc *httpModuleConfig) Run() error {
// TODO: Either prevent overlapping listeners on different servers, or combine them into one // 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 { for _, srv := range hc.Servers {
// set up the routes err := srv.Routes.setup()
for i, route := range srv.Routes { if err != nil {
// matchers return fmt.Errorf("setting up server routes: %v", err)
for modName, rawMsg := range route.Matchers { }
val, err := caddy2.LoadModule("http.matchers."+modName, rawMsg) err = srv.Errors.Routes.setup()
if err != nil { if err != nil {
return fmt.Errorf("loading matcher module '%s': %v", modName, err) return fmt.Errorf("setting up server error handling routes: %v", 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)
}
} }
s := &http.Server{ s := &http.Server{
@ -104,65 +84,56 @@ type httpServerConfig struct {
ReadTimeout caddy2.Duration `json:"read_timeout"` ReadTimeout caddy2.Duration `json:"read_timeout"`
ReadHeaderTimeout caddy2.Duration `json:"read_header_timeout"` ReadHeaderTimeout caddy2.Duration `json:"read_header_timeout"`
HiddenFiles []string `json:"hidden_files"` // TODO:... experimenting with shared/common state 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) { type httpErrorConfig struct {
var mid []Middleware // TODO: see about using make() for performance reasons Routes routeList `json:"routes"`
var responder Handler // TODO: some way to configure the logging of errors, probably? standardize the logging configuration first.
mrw := &middlewareResponseWriter{ResponseWriterWrapper: &ResponseWriterWrapper{w}} }
for _, route := range s.Routes { // ServeHTTP is the entry point for all HTTP requests.
matched := len(route.matchers) == 0 func (s httpServerConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) {
for _, m := range route.matchers { stack := s.Routes.buildMiddlewareChain(w, r)
if m.Match(r) { err := executeMiddlewareChain(w, r, stack)
matched = true if err != nil {
break // 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 { // executeMiddlewareChain executes stack with w and r. This function handles
Matchers map[string]json.RawMessage `json:"match"` // the special ErrRehandle error value, which reprocesses requests through
Apply []json.RawMessage `json:"apply"` // the stack again. Any error value returned from this function would be an
Respond json.RawMessage `json:"respond"` // actual error that needs to be handled.
func executeMiddlewareChain(w http.ResponseWriter, r *http.Request, stack Handler) error {
// decoded values const maxRehandles = 3
matchers []RouteMatcher var err error
middleware []MiddlewareHandler for i := 0; i < maxRehandles; i++ {
responder Handler 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. // 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) 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) { func parseListenAddr(a string) (network string, addrs []string, err error) {
network = "tcp" network = "tcp"
if idx := strings.Index(a, "/"); idx >= 0 { if idx := strings.Index(a, "/"); idx >= 0 {

View File

@ -13,6 +13,7 @@ func init() {
caddy2.RegisterModule(caddy2.Module{ caddy2.RegisterModule(caddy2.Module{
Name: "http.middleware.log", Name: "http.middleware.log",
New: func() (interface{}, error) { return new(Log), nil }, New: func() (interface{}, error) { return new(Log), nil },
// TODO: Examples of OnLoad and OnUnload.
OnLoad: func(instances []interface{}, priorState interface{}) (interface{}, error) { OnLoad: func(instances []interface{}, priorState interface{}) (interface{}, error) {
var counter int var counter int
if priorState != nil { if priorState != nil {
@ -42,6 +43,17 @@ type Log struct {
func (l *Log) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { func (l *Log) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
start := time.Now() 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 { if err := next.ServeHTTP(w, r); err != nil {
return err return err
} }

105
modules/caddyhttp/errors.go Normal file
View File

@ -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")

View File

@ -137,6 +137,7 @@ func (m matchHeader) Match(r *http.Request) bool {
return false return false
} }
// Interface guards
var ( var (
_ RouteMatcher = matchHost{} _ RouteMatcher = matchHost{}
_ RouteMatcher = matchPath{} _ RouteMatcher = matchPath{}

106
modules/caddyhttp/routes.go Normal file
View File

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