mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-26 21:53:48 +03: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
|
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 (
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ RouteMatcher = matchHost{}
|
_ RouteMatcher = matchHost{}
|
||||||
_ RouteMatcher = matchPath{}
|
_ 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