Flatten HTTP handler config (#2662)

Differentiating middleware and responders has one benefit, namely that
it's clear which module provides the response, but even then it's not
a great advantage. Linear handler config makes a little more sense,
giving greater flexibility and simplifying the core a bit, even though
it's slightly awkward that handlers which are responders may not use
the 'next' handler that is passed in at all.
This commit is contained in:
Matthew Holt 2019-07-09 12:58:39 -06:00
parent 6dfba5fda8
commit 4a3a418156
No known key found for this signature in database
GPG key ID: 2A349DD577D586A5
9 changed files with 99 additions and 148 deletions

View file

@ -322,13 +322,15 @@ func (app *App) automaticHTTPS() error {
MatchHost(domains), MatchHost(domains),
}, },
}, },
responder: Static{ handlers: []MiddlewareHandler{
StatusCode: http.StatusTemporaryRedirect, // TODO: use permanent redirect instead Static{
Headers: http.Header{ StatusCode: strconv.Itoa(http.StatusTemporaryRedirect), // TODO: use permanent redirect instead
"Location": []string{redirTo}, Headers: http.Header{
"Connection": []string{"close"}, "Location": []string{redirTo},
"Connection": []string{"close"},
},
Close: true,
}, },
Close: true,
}, },
}) })
} }
@ -381,34 +383,18 @@ func (app *App) listenerTaken(network, address string) bool {
var defaultALPN = []string{"h2", "http/1.1"} var defaultALPN = []string{"h2", "http/1.1"}
// RequestMatcher is a type that can match to a request. // RequestMatcher is a type that can match to a request.
// A route matcher MUST NOT modify the request. // A route matcher MUST NOT modify the request, with the
// only exception being its context.
type RequestMatcher interface { type RequestMatcher interface {
Match(*http.Request) bool Match(*http.Request) bool
} }
// Middleware chains one Handler to the next by being passed
// the next Handler in the chain.
type Middleware func(HandlerFunc) HandlerFunc
// MiddlewareHandler is a Handler that includes a reference
// to the next middleware handler in the chain. Middleware
// handlers MUST NOT call Write() or WriteHeader() on the
// response writer; doing so will panic. See Handler godoc
// for more information.
type MiddlewareHandler interface {
ServeHTTP(http.ResponseWriter, *http.Request, Handler) error
}
// Handler is like http.Handler except ServeHTTP may return an error. // Handler is like http.Handler except ServeHTTP may return an error.
// //
// Middleware and responder handlers both implement this method.
// Middleware must not call Write or WriteHeader on the ResponseWriter;
// doing so will cause a panic. Responders should write to the response
// if there was not an error.
//
// If any handler encounters an error, it should be returned for proper // If any handler encounters an error, it should be returned for proper
// handling. Return values should be propagated down the middleware chain // handling. Return values should be propagated down the middleware chain
// by returning it unchanged. Returned errors should not be re-wrapped. // by returning it unchanged. Returned errors should not be re-wrapped
// if they are already HandlerError values.
type Handler interface { type Handler interface {
ServeHTTP(http.ResponseWriter, *http.Request) error ServeHTTP(http.ResponseWriter, *http.Request) error
} }
@ -421,9 +407,25 @@ 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 // Middleware chains one Handler to the next by being passed
// sometimes better than a nil Handler pointer. // the next Handler in the chain.
var emptyHandler HandlerFunc = func(w http.ResponseWriter, r *http.Request) error { return nil } type Middleware func(HandlerFunc) HandlerFunc
// MiddlewareHandler is like Handler except it takes as a third
// argument the next handler in the chain. The next handler will
// never be nil, but may be a no-op handler if this is the last
// handler in the chain. Handlers which act as middleware should
// call the next handler's ServeHTTP method so as to propagate
// the request down the chain properly. Handlers which act as
// responders (content origins) need not invoke the next handler,
// since the last handler in the chain should be the first to
// write the response.
type MiddlewareHandler interface {
ServeHTTP(http.ResponseWriter, *http.Request, Handler) error
}
// emptyHandler is used as a no-op handler.
var emptyHandler HandlerFunc = func(http.ResponseWriter, *http.Request) error { return nil }
const ( const (
// DefaultHTTPPort is the default port for HTTP. // DefaultHTTPPort is the default port for HTTP.

View file

@ -36,7 +36,7 @@ import (
func init() { func init() {
caddy.RegisterModule(caddy.Module{ caddy.RegisterModule(caddy.Module{
Name: "http.middleware.encode", Name: "http.handlers.encode",
New: func() interface{} { return new(Encode) }, New: func() interface{} { return new(Encode) },
}) })
} }

View file

@ -35,7 +35,7 @@ func init() {
weakrand.Seed(time.Now().UnixNano()) weakrand.Seed(time.Now().UnixNano())
caddy.RegisterModule(caddy.Module{ caddy.RegisterModule(caddy.Module{
Name: "http.responders.file_server", Name: "http.handlers.file_server",
New: func() interface{} { return new(FileServer) }, New: func() interface{} { return new(FileServer) },
}) })
} }
@ -108,7 +108,7 @@ func (fsrv *FileServer) Validate() error {
return nil return nil
} }
func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) error { func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
filesToHide := fsrv.transformHidePaths(repl) filesToHide := fsrv.transformHidePaths(repl)
@ -119,7 +119,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) error
if filename == "" { if filename == "" {
// no files worked, so resort to fallback // no files worked, so resort to fallback
if fsrv.Fallback != nil { if fsrv.Fallback != nil {
fallback, w := fsrv.Fallback.BuildCompositeRoute(w, r) fallback := fsrv.Fallback.BuildCompositeRoute(w, r)
return fallback.ServeHTTP(w, r) return fallback.ServeHTTP(w, r)
} }
return caddyhttp.Error(http.StatusNotFound, nil) return caddyhttp.Error(http.StatusNotFound, nil)
@ -452,7 +452,7 @@ const minBackoff, maxBackoff = 2, 5
// Interface guards // Interface guards
var ( var (
_ caddy.Provisioner = (*FileServer)(nil) _ caddy.Provisioner = (*FileServer)(nil)
_ caddy.Validator = (*FileServer)(nil) _ caddy.Validator = (*FileServer)(nil)
_ caddyhttp.Handler = (*FileServer)(nil) _ caddyhttp.MiddlewareHandler = (*FileServer)(nil)
) )

View file

@ -25,7 +25,7 @@ import (
func init() { func init() {
caddy.RegisterModule(caddy.Module{ caddy.RegisterModule(caddy.Module{
Name: "http.middleware.rewrite", Name: "http.handlers.rewrite",
New: func() interface{} { return new(Rewrite) }, New: func() interface{} { return new(Rewrite) },
}) })
} }

View file

@ -28,25 +28,19 @@ import (
type ServerRoute struct { type ServerRoute struct {
Group string `json:"group,omitempty"` Group string `json:"group,omitempty"`
MatcherSets []map[string]json.RawMessage `json:"match,omitempty"` MatcherSets []map[string]json.RawMessage `json:"match,omitempty"`
Apply []json.RawMessage `json:"apply,omitempty"` Handle []json.RawMessage `json:"handle,omitempty"`
Respond json.RawMessage `json:"respond,omitempty"` Terminal bool `json:"terminal,omitempty"`
Terminal bool `json:"terminal,omitempty"`
// decoded values // decoded values
matcherSets []MatcherSet matcherSets []MatcherSet
middleware []MiddlewareHandler handlers []MiddlewareHandler
responder Handler
} }
// Empty returns true if the route has all zero/default values. // Empty returns true if the route has all zero/default values.
func (sr ServerRoute) Empty() bool { func (sr ServerRoute) Empty() bool {
return len(sr.MatcherSets) == 0 && return len(sr.MatcherSets) == 0 &&
len(sr.Apply) == 0 && len(sr.Handle) == 0 &&
len(sr.Respond) == 0 && len(sr.handlers) == 0 &&
len(sr.matcherSets) == 0 &&
len(sr.middleware) == 0 &&
sr.responder == nil &&
!sr.Terminal && !sr.Terminal &&
sr.Group == "" sr.Group == ""
} }
@ -98,40 +92,27 @@ func (routes RouteList) Provision(ctx caddy.Context) error {
} }
routes[i].MatcherSets = nil // allow GC to deallocate - TODO: Does this help? routes[i].MatcherSets = nil // allow GC to deallocate - TODO: Does this help?
// middleware // handlers
for j, rawMsg := range route.Apply { for j, rawMsg := range route.Handle {
mid, err := ctx.LoadModuleInline("middleware", "http.middleware", rawMsg) mh, err := ctx.LoadModuleInline("handler", "http.handlers", rawMsg)
if err != nil { if err != nil {
return fmt.Errorf("loading middleware module in position %d: %v", j, err) return fmt.Errorf("loading handler module in position %d: %v", j, err)
} }
routes[i].middleware = append(routes[i].middleware, mid.(MiddlewareHandler)) routes[i].handlers = append(routes[i].handlers, mh.(MiddlewareHandler))
} }
routes[i].Apply = nil // allow GC to deallocate - TODO: Does this help? routes[i].Handle = nil // allow GC to deallocate - TODO: Does this help?
// responder
if route.Respond != nil {
resp, err := ctx.LoadModuleInline("responder", "http.responders", route.Respond)
if err != nil {
return fmt.Errorf("loading responder module: %v", err)
}
routes[i].responder = resp.(Handler)
}
routes[i].Respond = nil // allow GC to deallocate - TODO: Does this help?
} }
return nil return nil
} }
// BuildCompositeRoute creates a chain of handlers by applying all the matching // BuildCompositeRoute creates a chain of handlers by
// routes. The returned ResponseWriter should be used instead of rw. // applying all of the matching routes.
func (routes RouteList) BuildCompositeRoute(rw http.ResponseWriter, req *http.Request) (Handler, http.ResponseWriter) { func (routes RouteList) BuildCompositeRoute(rw http.ResponseWriter, req *http.Request) Handler {
mrw := &middlewareResponseWriter{ResponseWriterWrapper: &ResponseWriterWrapper{rw}}
if len(routes) == 0 { if len(routes) == 0 {
return emptyHandler, mrw return emptyHandler
} }
var mid []Middleware var mid []Middleware
var responder Handler
groups := make(map[string]struct{}) groups := make(map[string]struct{})
for _, route := range routes { for _, route := range routes {
@ -140,9 +121,8 @@ func (routes RouteList) BuildCompositeRoute(rw http.ResponseWriter, req *http.Re
continue continue
} }
// if route is part of a group, ensure only // if route is part of a group, ensure only the
// the first matching route in the group is // first matching route in the group is applied
// applied
if route.Group != "" { if route.Group != "" {
_, ok := groups[route.Group] _, ok := groups[route.Group]
if ok { if ok {
@ -155,78 +135,48 @@ func (routes RouteList) BuildCompositeRoute(rw http.ResponseWriter, req *http.Re
} }
// apply the rest of the route // apply the rest of the route
for _, m := range route.middleware { for _, mh := range route.handlers {
// we have to be sure to wrap m outside // we have to be sure to wrap mh outside
// of our current scope so that the // of our current stack frame so that the
// reference to this m isn't overwritten // reference to this mh isn't overwritten
// on the next iteration, leaving only // on the next iteration, leaving the last
// the last middleware in the chain as // middleware in the chain as the ONLY
// the ONLY middleware in the chain! // middleware in the chain!
mid = append(mid, wrapMiddleware(m)) mid = append(mid, wrapMiddleware(mh))
}
if responder == nil {
responder = route.responder
} }
// if this route is supposed to be last, don't
// compile any more into the chain
if route.Terminal { if route.Terminal {
break break
} }
} }
// build the middleware stack, with the responder at the end // build the middleware chain, with the responder at the end
stack := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { stack := emptyHandler
if responder == nil {
return nil
}
mrw.allowWrites = true
return responder.ServeHTTP(w, r)
})
for i := len(mid) - 1; i >= 0; i-- { for i := len(mid) - 1; i >= 0; i-- {
stack = mid[i](stack) stack = mid[i](stack)
} }
return stack, mrw return stack
} }
// wrapMiddleware wraps m such that it can be correctly // wrapMiddleware wraps m such that it can be correctly
// appended to a list of middleware. This separate closure // appended to a list of middleware. We can't do this
// is necessary so that only the last middleware in a loop // directly in a loop because it relies on a reference
// does not become the only middleware of the stack, // to mh not changing until the execution of its handler,
// repeatedly executed (i.e. it is necessary to keep a // which is deferred by multiple func closures. In other
// reference to this m outside of the scope of a loop)! // words, we need to pull this particular MiddlewareHandler
func wrapMiddleware(m MiddlewareHandler) Middleware { // pointer into its own stack frame to preserve it so it
// won't be overwritten in future loop iterations.
func wrapMiddleware(mh MiddlewareHandler) Middleware {
return func(next HandlerFunc) HandlerFunc { return func(next HandlerFunc) HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
// TODO: This is where request tracing could be implemented; also // TODO: This is where request tracing could be implemented; also
// see below to trace the responder as well // see below to trace the responder as well
// TODO: Trace a diff of the request, would be cool too! see what changed since the last middleware (host, headers, URI...) // TODO: Trace a diff of the request, would be cool too! see what changed since the last middleware (host, headers, URI...)
// TODO: see what the std lib gives us in terms of stack tracing too // TODO: see what the std lib gives us in terms of stack tracing too
return m.ServeHTTP(w, r, next) return mh.ServeHTTP(w, r, next)
} }
} }
} }
type middlewareResponseWriter struct {
*ResponseWriterWrapper
allowWrites bool
}
func (mrw middlewareResponseWriter) WriteHeader(statusCode int) {
if !mrw.allowWrites {
// technically, this is not true: middleware can write headers,
// but only after the responder handler has returned; either the
// responder did nothing with the response (sad face), or the
// middleware wrapped the response and deferred the write
panic("WriteHeader: middleware cannot write response headers")
}
mrw.ResponseWriterWrapper.WriteHeader(statusCode)
}
func (mrw middlewareResponseWriter) Write(b []byte) (int, error) {
if !mrw.allowWrites {
panic("Write: middleware cannot write to the response before responder")
}
return mrw.ResponseWriterWrapper.Write(b)
}
// Interface guard
var _ HTTPInterfaces = (*middlewareResponseWriter)(nil)

View file

@ -65,9 +65,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
addHTTPVarsToReplacer(repl, r, w) addHTTPVarsToReplacer(repl, r, w)
// build and execute the main handler chain // build and execute the main handler chain
stack, wrappedWriter := s.Routes.BuildCompositeRoute(w, r) stack := s.Routes.BuildCompositeRoute(w, r)
stack = s.wrapPrimaryRoute(stack) stack = s.wrapPrimaryRoute(stack)
err := s.executeCompositeRoute(wrappedWriter, r, stack) err := s.executeCompositeRoute(w, r, stack)
if err != nil { if err != nil {
// add the raw error value to the request context // add the raw error value to the request context
// so it can be accessed by error handlers // so it can be accessed by error handlers
@ -85,8 +85,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
if s.Errors != nil && len(s.Errors.Routes) > 0 { if s.Errors != nil && len(s.Errors.Routes) > 0 {
errStack, wrappedWriter := s.Errors.Routes.BuildCompositeRoute(w, r) errStack := s.Errors.Routes.BuildCompositeRoute(w, r)
err := s.executeCompositeRoute(wrappedWriter, r, errStack) err := s.executeCompositeRoute(w, r, errStack)
if err != nil { if err != nil {
// TODO: what should we do if the error handler has an error? // TODO: what should we do if the error handler has an error?
log.Printf("[ERROR] [%s %s] handling error: %v", r.Method, r.RequestURI, err) log.Printf("[ERROR] [%s %s] handling error: %v", r.Method, r.RequestURI, err)
@ -154,6 +154,8 @@ func (s *Server) enforcementHandler(w http.ResponseWriter, r *http.Request, next
return next.ServeHTTP(w, r) return next.ServeHTTP(w, r)
} }
// listenersUseAnyPortOtherThan returns true if there are any
// listeners in s that use a port which is not otherPort.
func (s *Server) listenersUseAnyPortOtherThan(otherPort int) bool { func (s *Server) listenersUseAnyPortOtherThan(otherPort int) bool {
for _, lnAddr := range s.Listen { for _, lnAddr := range s.Listen {
_, addrs, err := caddy.ParseListenAddr(lnAddr) _, addrs, err := caddy.ParseListenAddr(lnAddr)

View file

@ -24,21 +24,20 @@ import (
func init() { func init() {
caddy.RegisterModule(caddy.Module{ caddy.RegisterModule(caddy.Module{
Name: "http.responders.static", Name: "http.handlers.static",
New: func() interface{} { return new(Static) }, New: func() interface{} { return new(Static) },
}) })
} }
// Static implements a simple responder for static responses. // Static implements a simple responder for static responses.
type Static struct { type Static struct {
StatusCode int `json:"status_code"` // TODO: should we turn this into a string so that only one field is needed? (string allows replacements) StatusCode string `json:"status_code"`
StatusCodeStr string `json:"status_code_str"` Headers http.Header `json:"headers"`
Headers http.Header `json:"headers"` Body string `json:"body"`
Body string `json:"body"` Close bool `json:"close"`
Close bool `json:"close"`
} }
func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error { func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error {
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
// close the connection after responding // close the connection after responding
@ -60,16 +59,13 @@ func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
} }
// get the status code // get the status code
statusCode := s.StatusCode statusCode := http.StatusOK
if statusCode == 0 && s.StatusCodeStr != "" { if s.StatusCode != "" {
intVal, err := strconv.Atoi(repl.ReplaceAll(s.StatusCodeStr, "")) intVal, err := strconv.Atoi(repl.ReplaceAll(s.StatusCode, ""))
if err == nil { if err == nil {
statusCode = intVal statusCode = intVal
} }
} }
if statusCode == 0 {
statusCode = http.StatusOK
}
// write headers // write headers
w.WriteHeader(statusCode) w.WriteHeader(statusCode)
@ -83,4 +79,4 @@ func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
} }
// Interface guard // Interface guard
var _ Handler = (*Static)(nil) var _ MiddlewareHandler = (*Static)(nil)

View file

@ -19,6 +19,7 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strconv"
"testing" "testing"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
@ -29,7 +30,7 @@ func TestStaticResponseHandler(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
s := Static{ s := Static{
StatusCode: http.StatusNotFound, StatusCode: strconv.Itoa(http.StatusNotFound),
Headers: http.Header{ Headers: http.Header{
"X-Test": []string{"Testing"}, "X-Test": []string{"Testing"},
}, },
@ -37,7 +38,7 @@ func TestStaticResponseHandler(t *testing.T) {
Close: true, Close: true,
} }
err := s.ServeHTTP(w, r) err := s.ServeHTTP(w, r, nil)
if err != nil { if err != nil {
t.Errorf("did not expect an error, but got: %v", err) t.Errorf("did not expect an error, but got: %v", err)
} }

View file

@ -28,7 +28,7 @@ import (
func init() { func init() {
caddy.RegisterModule(caddy.Module{ caddy.RegisterModule(caddy.Module{
Name: "http.middleware.templates", Name: "http.handlers.templates",
New: func() interface{} { return new(Templates) }, New: func() interface{} { return new(Templates) },
}) })
} }