Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
Begin implementing error handling and re-handling
Matthew Holt committed
Apr 11, 2019 at 20:42 UTC
545f28008e0175491af030f8689cab2112fda9ed
6 files changed
+281
-84
caddy.go
+2
-4
@@ -161,10 +161,8 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
161
return nil
162
}
163
164
-// MarshalJSON satisfies json.Marshaler.
165
-func (d Duration) MarshalJSON() ([]byte, error) {
166
- return []byte(fmt.Sprintf(`"%s"`, time.Duration(d).String())), nil
167
-}
164
+// CtxKey is a value type for use with context.WithValue.
165
+type CtxKey string
166
167
// currentCfg is the currently-loaded configuration.
168
var (
modules/caddyhttp/caddyhttp.go
+55
-80
@@ -2,9 +2,9 @@ package caddyhttp
2
3
import (
4
"context"
5
- "encoding/json"
5
"fmt"
6
"log"
7
+ mathrand "math/rand"
8
"net"
9
"net/http"
10
"strconv"
@@ -22,6 +22,8 @@ func init() {
22
if err != nil {
23
log.Fatal(err)
24
}
25
+
26
+ mathrand.Seed(time.Now().UnixNano())
27
}
28
29
type httpModuleConfig struct {
@@ -32,36 +34,14 @@ type httpModuleConfig struct {
34
35
func (hc *httpModuleConfig) Run() error {
36
// TODO: Either prevent overlapping listeners on different servers, or combine them into one
35
- // TODO: A way to loop requests back through, so have them start the matching over again, but keeping any mutations
37
for _, srv := range hc.Servers {
37
- // set up the routes
38
- for i, route := range srv.Routes {
39
- // matchers
40
- for modName, rawMsg := range route.Matchers {
41
- val, err := caddy2.LoadModule("http.matchers."+modName, rawMsg)
42
- if err != nil {
43
- return fmt.Errorf("loading matcher module '%s': %v", modName, err)
44
- }
45
- srv.Routes[i].matchers = append(srv.Routes[i].matchers, val.(RouteMatcher))
46
- }
47
-
48
- // middleware
49
- for j, rawMsg := range route.Apply {
50
- mid, err := caddy2.LoadModuleInlineName("http.middleware", rawMsg)
51
- if err != nil {
52
- return fmt.Errorf("loading middleware module in position %d: %v", j, err)
53
- }
54
- srv.Routes[i].middleware = append(srv.Routes[i].middleware, mid.(MiddlewareHandler))
55
- }
56
-
57
- // responder
58
- if route.Respond != nil {
59
- resp, err := caddy2.LoadModuleInlineName("http.responders", route.Respond)
60
- if err != nil {
61
- return fmt.Errorf("loading responder module: %v", err)
62
- }
63
- srv.Routes[i].responder = resp.(Handler)
64
- }
38
+ err := srv.Routes.setup()
39
+ if err != nil {
40
+ return fmt.Errorf("setting up server routes: %v", err)
41
+ }
42
+ err = srv.Errors.Routes.setup()
43
+ if err != nil {
44
+ return fmt.Errorf("setting up server error handling routes: %v", err)
45
}
46
47
s := &http.Server{
@@ -104,65 +84,56 @@ type httpServerConfig struct {
84
ReadTimeout caddy2.Duration `json:"read_timeout"`
85
ReadHeaderTimeout caddy2.Duration `json:"read_header_timeout"`
86
HiddenFiles []string `json:"hidden_files"` // TODO:... experimenting with shared/common state
107
- Routes []serverRoute `json:"routes"`
87
+ Routes routeList `json:"routes"`
88
+ Errors httpErrorConfig `json:"errors"`
89
}
90
110
-func (s httpServerConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) {
111
- var mid []Middleware // TODO: see about using make() for performance reasons
112
- var responder Handler
113
- mrw := &middlewareResponseWriter{ResponseWriterWrapper: &ResponseWriterWrapper{w}}
91
+type httpErrorConfig struct {
92
+ Routes routeList `json:"routes"`
93
+ // TODO: some way to configure the logging of errors, probably? standardize the logging configuration first.
94
+}
95
115
- for _, route := range s.Routes {
116
- matched := len(route.matchers) == 0
117
- for _, m := range route.matchers {
118
- if m.Match(r) {
119
- matched = true
120
- break
96
+// ServeHTTP is the entry point for all HTTP requests.
97
+func (s httpServerConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) {
98
+ stack := s.Routes.buildMiddlewareChain(w, r)
99
+ err := executeMiddlewareChain(w, r, stack)
100
+ if err != nil {
101
+ // add the error value to the request context so
102
+ // it can be accessed by error handlers
103
+ c := context.WithValue(r.Context(), ErrorCtxKey, err)
104
+ r = r.WithContext(c)
105
+
106
+ if len(s.Errors.Routes) == 0 {
107
+ // TODO: implement a default error handler?
108
+ log.Printf("[ERROR] %s", err)
109
+ } else {
110
+ errStack := s.Errors.Routes.buildMiddlewareChain(w, r)
111
+ err := executeMiddlewareChain(w, r, errStack)
112
+ if err != nil {
113
+ // TODO: what should we do if the error handler has an error?
114
+ log.Printf("[ERROR] handling error: %v", err)
115
}
116
}
123
- if !matched {
124
- continue
125
- }
126
- for _, m := range route.middleware {
127
- mid = append(mid, func(next HandlerFunc) HandlerFunc {
128
- return func(w http.ResponseWriter, r *http.Request) error {
129
- return m.ServeHTTP(mrw, r, next)
130
- }
131
- })
132
- }
133
- if responder == nil {
134
- responder = route.responder
135
- }
117
}
118
+}
119
138
- // build the middleware stack, with the responder at the end
139
- stack := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
140
- if responder == nil {
141
- return nil
120
+// executeMiddlewareChain executes stack with w and r. This function handles
121
+// the special ErrRehandle error value, which reprocesses requests through
122
+// the stack again. Any error value returned from this function would be an
123
+// actual error that needs to be handled.
124
+func executeMiddlewareChain(w http.ResponseWriter, r *http.Request, stack Handler) error {
125
+ const maxRehandles = 3
126
+ var err error
127
+ for i := 0; i < maxRehandles; i++ {
128
+ err = stack.ServeHTTP(w, r)
129
+ if err != ErrRehandle {
130
+ break
131
+ }
132
+ if i == maxRehandles-1 {
133
+ return fmt.Errorf("too many rehandles")
134
}
143
- mrw.allowWrites = true
144
- return responder.ServeHTTP(w, r)
145
- })
146
- for i := len(mid) - 1; i >= 0; i-- {
147
- stack = mid[i](stack)
148
- }
149
-
150
- err := stack.ServeHTTP(w, r)
151
- if err != nil {
152
- // TODO: error handling
153
- log.Printf("[ERROR] TODO: error handling: %v", err)
135
}
155
-}
156
-
157
-type serverRoute struct {
158
- Matchers map[string]json.RawMessage `json:"match"`
159
- Apply []json.RawMessage `json:"apply"`
160
- Respond json.RawMessage `json:"respond"`
161
-
162
- // decoded values
163
- matchers []RouteMatcher
164
- middleware []MiddlewareHandler
165
- responder Handler
136
+ return err
137
}
138
139
// 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 {
177
return f(w, r)
178
}
179
180
+// emptyHandler is used as a no-op handler, which is
181
+// sometimes better than a nil Handler pointer.
182
+var emptyHandler HandlerFunc = func(w http.ResponseWriter, r *http.Request) error { return nil }
183
+
184
func parseListenAddr(a string) (network string, addrs []string, err error) {
185
network = "tcp"
186
if idx := strings.Index(a, "/"); idx >= 0 {
modules/caddyhttp/caddylog/log.go
+12
@@ -13,6 +13,7 @@ func init() {
13
caddy2.RegisterModule(caddy2.Module{
14
Name: "http.middleware.log",
15
New: func() (interface{}, error) { return new(Log), nil },
16
+ // TODO: Examples of OnLoad and OnUnload.
17
OnLoad: func(instances []interface{}, priorState interface{}) (interface{}, error) {
18
var counter int
19
if priorState != nil {
@@ -42,6 +43,17 @@ type Log struct {
43
func (l *Log) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
44
start := time.Now()
45
46
+ // TODO: An example of returning errors
47
+ // return caddyhttp.Error(http.StatusBadRequest, fmt.Errorf("this is a basic error"))
48
+ // return caddyhttp.Error(http.StatusBadGateway, caddyhttp.HandlerError{
49
+ // Err: fmt.Errorf("this is a detailed error"),
50
+ // Message: "We had trouble doing the thing.",
51
+ // Recommendations: []string{
52
+ // "Try reconnecting the gizbop.",
53
+ // "Turn off the Internet.",
54
+ // },
55
+ // })
56
+
57
if err := next.ServeHTTP(w, r); err != nil {
58
return err
59
}
modules/caddyhttp/errors.go
new
+105
@@ -0,0 +1,105 @@
1
+package caddyhttp
2
+
3
+import (
4
+ "fmt"
5
+ mathrand "math/rand"
6
+ "path"
7
+ "runtime"
8
+ "strings"
9
+
10
+ "bitbucket.org/lightcodelabs/caddy2"
11
+)
12
+
13
+// Error is a convenient way for a Handler to populate the
14
+// essential fields of a HandlerError. If err is itself a
15
+// HandlerError, then any essential fields that are not
16
+// set will be populated.
17
+func Error(statusCode int, err error) HandlerError {
18
+ const idLen = 9
19
+ if he, ok := err.(HandlerError); ok {
20
+ if he.ID == "" {
21
+ he.ID = randString(idLen, true)
22
+ }
23
+ if he.Trace == "" {
24
+ he.Trace = trace()
25
+ }
26
+ if he.StatusCode == 0 {
27
+ he.StatusCode = statusCode
28
+ }
29
+ return he
30
+ }
31
+ return HandlerError{
32
+ ID: randString(idLen, true),
33
+ StatusCode: statusCode,
34
+ Err: err,
35
+ Trace: trace(),
36
+ }
37
+}
38
+
39
+// HandlerError is a serializable representation of
40
+// an error from within an HTTP handler.
41
+type HandlerError struct {
42
+ Err error // the original error value and message
43
+ StatusCode int // the HTTP status code to associate with this error
44
+ Message string // an optional message that can be shown to the user
45
+ Recommendations []string // an optional list of things to try to resolve the error
46
+
47
+ ID string // generated; for identifying this error in logs
48
+ Trace string // produced from call stack
49
+}
50
+
51
+func (e HandlerError) Error() string {
52
+ var s string
53
+ if e.ID != "" {
54
+ s += fmt.Sprintf("{id=%s}", e.ID)
55
+ }
56
+ if e.Trace != "" {
57
+ s += " " + e.Trace
58
+ }
59
+ if e.StatusCode != 0 {
60
+ s += fmt.Sprintf(": HTTP %d", e.StatusCode)
61
+ }
62
+ if e.Err != nil {
63
+ s += ": " + e.Err.Error()
64
+ }
65
+ return strings.TrimSpace(s)
66
+}
67
+
68
+// randString returns a string of n random characters.
69
+// It is not even remotely secure OR a proper distribution.
70
+// But it's good enough for some things. It excludes certain
71
+// confusing characters like I, l, 1, 0, O, etc. If sameCase
72
+// is true, then uppercase letters are excluded.
73
+func randString(n int, sameCase bool) string {
74
+ if n <= 0 {
75
+ return ""
76
+ }
77
+ dict := []byte("abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY23456789")
78
+ if sameCase {
79
+ dict = []byte("abcdefghijkmnpqrstuvwxyz0123456789")
80
+ }
81
+ b := make([]byte, n)
82
+ for i := range b {
83
+ b[i] = dict[mathrand.Int63()%int64(len(dict))]
84
+ }
85
+ return string(b)
86
+}
87
+
88
+func trace() string {
89
+ if pc, file, line, ok := runtime.Caller(2); ok {
90
+ filename := path.Base(file)
91
+ pkgAndFuncName := path.Base(runtime.FuncForPC(pc).Name())
92
+ return fmt.Sprintf("%s (%s:%d)", pkgAndFuncName, filename, line)
93
+ }
94
+ return ""
95
+}
96
+
97
+// ErrRehandle is a special error value that Handlers should return
98
+// from their ServeHTTP() method if the request is to be re-processed.
99
+// This error value is a sentinel value that should not be wrapped or
100
+// modified.
101
+var ErrRehandle = fmt.Errorf("rehandling request")
102
+
103
+// ErrorCtxKey is the context key to use when storing
104
+// an error (for use with context.Context).
105
+const ErrorCtxKey = caddy2.CtxKey("handler_chain_error")
modules/caddyhttp/matchers.go
+1
@@ -137,6 +137,7 @@ func (m matchHeader) Match(r *http.Request) bool {
137
return false
138
}
139
140
+// Interface guards
141
var (
142
_ RouteMatcher = matchHost{}
143
_ RouteMatcher = matchPath{}
modules/caddyhttp/routes.go
new
+106
@@ -0,0 +1,106 @@
1
+package caddyhttp
2
+
3
+import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "net/http"
7
+
8
+ "bitbucket.org/lightcodelabs/caddy2"
9
+)
10
+
11
+type serverRoute struct {
12
+ Matchers map[string]json.RawMessage `json:"match"`
13
+ Apply []json.RawMessage `json:"apply"`
14
+ Respond json.RawMessage `json:"respond"`
15
+
16
+ Exclusive bool `json:"exclusive"`
17
+
18
+ // decoded values
19
+ matchers []RouteMatcher
20
+ middleware []MiddlewareHandler
21
+ responder Handler
22
+}
23
+
24
+type routeList []serverRoute
25
+
26
+func (routes routeList) buildMiddlewareChain(w http.ResponseWriter, r *http.Request) Handler {
27
+ if len(routes) == 0 {
28
+ return emptyHandler
29
+ }
30
+
31
+ var mid []Middleware
32
+ var responder Handler
33
+ mrw := &middlewareResponseWriter{ResponseWriterWrapper: &ResponseWriterWrapper{w}}
34
+
35
+ for _, route := range routes {
36
+ matched := len(route.matchers) == 0
37
+ for _, m := range route.matchers {
38
+ if m.Match(r) {
39
+ matched = true
40
+ break
41
+ }
42
+ }
43
+ if !matched {
44
+ continue
45
+ }
46
+ for _, m := range route.middleware {
47
+ mid = append(mid, func(next HandlerFunc) HandlerFunc {
48
+ return func(w http.ResponseWriter, r *http.Request) error {
49
+ return m.ServeHTTP(mrw, r, next)
50
+ }
51
+ })
52
+ }
53
+ if responder == nil {
54
+ responder = route.responder
55
+ }
56
+ if route.Exclusive {
57
+ break
58
+ }
59
+ }
60
+
61
+ // build the middleware stack, with the responder at the end
62
+ stack := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
63
+ if responder == nil {
64
+ return nil
65
+ }
66
+ mrw.allowWrites = true
67
+ return responder.ServeHTTP(w, r)
68
+ })
69
+ for i := len(mid) - 1; i >= 0; i-- {
70
+ stack = mid[i](stack)
71
+ }
72
+
73
+ return stack
74
+}
75
+
76
+func (routes routeList) setup() error {
77
+ for i, route := range routes {
78
+ // matchers
79
+ for modName, rawMsg := range route.Matchers {
80
+ val, err := caddy2.LoadModule("http.matchers."+modName, rawMsg)
81
+ if err != nil {
82
+ return fmt.Errorf("loading matcher module '%s': %v", modName, err)
83
+ }
84
+ routes[i].matchers = append(routes[i].matchers, val.(RouteMatcher))
85
+ }
86
+
87
+ // middleware
88
+ for j, rawMsg := range route.Apply {
89
+ mid, err := caddy2.LoadModuleInlineName("http.middleware", rawMsg)
90
+ if err != nil {
91
+ return fmt.Errorf("loading middleware module in position %d: %v", j, err)
92
+ }
93
+ routes[i].middleware = append(routes[i].middleware, mid.(MiddlewareHandler))
94
+ }
95
+
96
+ // responder
97
+ if route.Respond != nil {
98
+ resp, err := caddy2.LoadModuleInlineName("http.responders", route.Respond)
99
+ if err != nil {
100
+ return fmt.Errorf("loading responder module: %v", err)
101
+ }
102
+ routes[i].responder = resp.(Handler)
103
+ }
104
+ }
105
+ return nil
106
+}