mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-16 21:56:40 -05:00
Begin implementing error handling and re-handling
This commit is contained in:
parent
d42529348f
commit
545f28008e
6 changed files with 282 additions and 85 deletions
6
caddy.go
6
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 (
|
||||
|
|
|
@ -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)
|
||||
err := srv.Routes.setup()
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading matcher module '%s': %v", modName, err)
|
||||
return fmt.Errorf("setting up server 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)
|
||||
err = srv.Errors.Routes.setup()
|
||||
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)
|
||||
}
|
||||
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
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 !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 i == maxRehandles-1 {
|
||||
return fmt.Errorf("too many rehandles")
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
105
modules/caddyhttp/errors.go
Normal file
105
modules/caddyhttp/errors.go
Normal 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")
|
|
@ -137,6 +137,7 @@ func (m matchHeader) Match(r *http.Request) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ RouteMatcher = matchHost{}
|
||||
_ RouteMatcher = matchPath{}
|
||||
|
|
106
modules/caddyhttp/routes.go
Normal file
106
modules/caddyhttp/routes.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue