From 845982030542a0fd9c8e8cbf76a84c7482cb3755 Mon Sep 17 00:00:00 2001 From: Carl Jackson Date: Sun, 18 Jan 2015 15:31:22 -0800 Subject: [PATCH] Make C.Env a map[interface{}]interface{} This is a major breaking change to web.C, the Goji context object. The Env key has been changed from a map[string]interface{} to a map[interface{}]interface{} in order to better support package-private environment values and to make the context value more compatible with golang.org/x/net/context's Context. Since strings support equality, most existing uses of web.C should continue to function. Users who construct Env by hand (i.e., by calling "make") will need to update their code as instructed by their compiler. Users who iterate over the environment will need to update their code to take into account the fact that keys may no longer be strings. --- web/middleware/envinit.go | 31 +++++------ web/middleware/options_test.go | 2 +- web/middleware/realip.go | 14 +---- web/middleware/request_id.go | 2 +- web/middleware_test.go | 2 +- web/mux_test.go | 2 +- web/pattern.go | 4 +- web/router.go | 2 +- web/router_test.go | 2 +- web/web.go | 94 +++++++++++++++------------------- 10 files changed, 67 insertions(+), 88 deletions(-) diff --git a/web/middleware/envinit.go b/web/middleware/envinit.go index aefefb7..ae3b683 100644 --- a/web/middleware/envinit.go +++ b/web/middleware/envinit.go @@ -6,21 +6,22 @@ import ( "github.com/zenazn/goji/web" ) -// EnvInit is a middleware that allocates an environment map if one does not -// already exist. This is necessary because Goji does not guarantee that Env is -// present when running middleware (it avoids forcing the map allocation). Note -// that other middleware should check Env for nil in order to maximize -// compatibility (when EnvInit is not used, or when another middleware layer -// blanks out Env), but for situations in which the user controls the middleware -// stack and knows EnvInit is present, this middleware can eliminate a lot of -// boilerplate. -func EnvInit(c *web.C, h http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - if c.Env == nil { - c.Env = make(map[string]interface{}) - } - h.ServeHTTP(w, r) +type envInit struct { + c *web.C + h http.Handler +} + +func (e envInit) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if e.c.Env == nil { + e.c.Env = make(map[interface{}]interface{}) } + e.h.ServeHTTP(w, r) +} - return http.HandlerFunc(fn) +// EnvInit is a middleware that allocates an environment map if it is nil. While +// it's impossible in general to ensure that Env is never nil in a middleware +// stack, in most common cases placing this middleware at the top of the stack +// will eliminate the need for repetative nil checks. +func EnvInit(c *web.C, h http.Handler) http.Handler { + return envInit{c, h} } diff --git a/web/middleware/options_test.go b/web/middleware/options_test.go index 74f5481..e962145 100644 --- a/web/middleware/options_test.go +++ b/web/middleware/options_test.go @@ -21,7 +21,7 @@ func testOptions(r *http.Request, f func(*web.C, http.ResponseWriter, *http.Requ return w } -var optionsTestEnv = map[string]interface{}{ +var optionsTestEnv = map[interface{}]interface{}{ web.ValidMethodsKey: []string{ "hello", "world", diff --git a/web/middleware/realip.go b/web/middleware/realip.go index 99cd311..ae5599f 100644 --- a/web/middleware/realip.go +++ b/web/middleware/realip.go @@ -3,20 +3,14 @@ package middleware import ( "net/http" "strings" - - "github.com/zenazn/goji/web" ) -// Key the original value of RemoteAddr is stored under. -const OriginalRemoteAddrKey = "originalRemoteAddr" - var xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For") var xRealIP = http.CanonicalHeaderKey("X-Real-IP") // RealIP is a middleware that sets a http.Request's RemoteAddr to the results // of parsing either the X-Forwarded-For header or the X-Real-IP header (in that -// order). It places the original value of RemoteAddr in a context environment -// variable. +// order). // // This middleware should be inserted fairly early in the middleware stack to // ensure that subsequent layers (e.g., request loggers) which examine the @@ -29,13 +23,9 @@ var xRealIP = http.CanonicalHeaderKey("X-Real-IP") // values from the client, or if you use this middleware without a reverse // proxy, malicious clients will be able to make you very sad (or, depending on // how you're using RemoteAddr, vulnerable to an attack of some sort). -func RealIP(c *web.C, h http.Handler) http.Handler { +func RealIP(h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { if rip := realIP(r); rip != "" { - if c.Env == nil { - c.Env = make(map[string]interface{}) - } - c.Env[OriginalRemoteAddrKey] = r.RemoteAddr r.RemoteAddr = rip } h.ServeHTTP(w, r) diff --git a/web/middleware/request_id.go b/web/middleware/request_id.go index 27e2313..834d8e3 100644 --- a/web/middleware/request_id.go +++ b/web/middleware/request_id.go @@ -60,7 +60,7 @@ func init() { func RequestID(c *web.C, h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { if c.Env == nil { - c.Env = make(map[string]interface{}) + c.Env = make(map[interface{}]interface{}) } myid := atomic.AddUint64(&reqid, 1) c.Env[RequestIDKey] = fmt.Sprintf("%s-%06d", prefix, myid) diff --git a/web/middleware_test.go b/web/middleware_test.go index b262b07..678ccaa 100644 --- a/web/middleware_test.go +++ b/web/middleware_test.go @@ -179,7 +179,7 @@ func TestContext(t *testing.T) { if c.Env != nil || c.URLParams != nil { t.Error("Expected a clean context") } - c.Env = make(map[string]interface{}) + c.Env = make(map[interface{}]interface{}) c.Env["reqID"] = 1 h.ServeHTTP(w, r) diff --git a/web/mux_test.go b/web/mux_test.go index 1854524..f461452 100644 --- a/web/mux_test.go +++ b/web/mux_test.go @@ -36,7 +36,7 @@ func TestIfItWorks(t *testing.T) { } r, _ = http.NewRequest("GET", "/hello/bob", nil) - env := map[string]interface{}{"greeting": "Yo "} + env := map[interface{}]interface{}{"greeting": "Yo "} m.ServeHTTPC(C{Env: env}, httptest.NewRecorder(), r) out = <-ch if out != "Yo bob" { diff --git a/web/pattern.go b/web/pattern.go index 99ca59e..c277ae1 100644 --- a/web/pattern.go +++ b/web/pattern.go @@ -56,7 +56,9 @@ of three types: that is anchored on the left (i.e., the beginning of the string). If your regular expression is not anchored on the left, a hopefully-identical left-anchored regular expression will be created - and used instead. + and used instead. Named capturing groups will bind URLParams of the + same name; unnamed capturing groups will be bound to the variables + "$1", "$2", etc. ParsePattern fatally exits (using log.Fatalf) if it is passed a value of an unexpected type. It is the caller's responsibility to ensure that ParsePattern diff --git a/web/router.go b/web/router.go index c291294..4659d85 100644 --- a/web/router.go +++ b/web/router.go @@ -129,7 +129,7 @@ func (rt *router) route(c *C, w http.ResponseWriter, r *http.Request) { sort.Strings(methodsList) if c.Env == nil { - c.Env = map[string]interface{}{ + c.Env = map[interface{}]interface{}{ ValidMethodsKey: methodsList, } } else { diff --git a/web/router_test.go b/web/router_test.go index 44dabf5..1d2c27e 100644 --- a/web/router_test.go +++ b/web/router_test.go @@ -310,7 +310,7 @@ func TestValidMethods(t *testing.T) { // This should also work when c.Env has already been initalized m.Use(func(c *C, h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c.Env = make(map[string]interface{}) + c.Env = make(map[interface{}]interface{}) h.ServeHTTP(w, r) }) }) diff --git a/web/web.go b/web/web.go index 480f670..81a2bae 100644 --- a/web/web.go +++ b/web/web.go @@ -1,26 +1,18 @@ /* -Package web implements a fast and flexible middleware stack and mux. - -The underlying philosophy behind this package is that net/http is a very good -HTTP library which is only missing a few features. If you disagree with this -statement (e.g., you think that the interfaces it exposes are not especially -good, or if you're looking for a comprehensive "batteries included" feature -list), you're likely not going to have a good time using this library. In that -spirit, we have attempted wherever possible to be compatible with net/http. You -should be able to insert any net/http compliant handler into this library, or -use this library with any other net/http compliant mux. +Package web provides a fast and flexible middleware stack and mux. This package attempts to solve three problems that net/http does not. First, it -allows you to specify URL patterns with Sinatra-like named wildcards and -regexps. Second, it allows you to write reconfigurable middleware stacks. And -finally, it allows you to attach additional context to requests, in a manner -that can be manipulated by both compliant middleware and handlers. +allows you to specify flexible patterns, including routes with named parameters +and regular expressions. Second, it allows you to write reconfigurable +middleware stacks. And finally, it allows you to attach additional context to +requests, in a manner that can be manipulated by both compliant middleware and +handlers. A usage example: m := web.New() -Use your favorite HTTP verbs: +Use your favorite HTTP verbs and the interfaces you know and love from net/http: var legacyFooHttpHandler http.Handler // From elsewhere m.Get("/foo", legacyFooHttpHandler) @@ -28,7 +20,7 @@ Use your favorite HTTP verbs: w.Write([]byte("Hello world!")) }) -Bind parameters using either Sinatra-like patterns or regular expressions: +Bind parameters using either named captures or regular expressions: m.Get("/hello/:name", func(c web.C, w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, %s!", c.URLParams["name"]) @@ -41,33 +33,32 @@ Bind parameters using either Sinatra-like patterns or regular expressions: Middleware are functions that wrap http.Handlers, just like you'd use with raw net/http. Middleware functions can optionally take a context parameter, which will be threaded throughout the middleware stack and to the final handler, even -if not all of these things support contexts. Middleware are encouraged to use -the Env parameter to pass data to other middleware and to the final handler: +if not all of the intervening middleware or handlers support contexts. +Middleware are encouraged to use the Env parameter to pass request-scoped data +to other middleware and to the final handler: - m.Use(func(h http.Handler) http.Handler { + func LoggerMiddleware(h http.Handler) http.Handler { handler := func(w http.ResponseWriter, r *http.Request) { log.Println("Before request") h.ServeHTTP(w, r) log.Println("After request") } return http.HandlerFunc(handler) - }) - m.Use(func(c *web.C, h http.Handler) http.Handler { + } + func AuthMiddleware(c *web.C, h http.Handler) http.Handler { handler := func(w http.ResponseWriter, r *http.Request) { - cookie, err := r.Cookie("user") - if err == nil { - // Consider using the middleware EnvInit instead - // of repeating the below check - if c.Env == nil { - c.Env = make(map[string]interface{}) - } + if cookie, err := r.Cookie("user"); err == nil { c.Env["user"] = cookie.Value } h.ServeHTTP(w, r) } return http.HandlerFunc(handler) - }) + } + // This makes the AuthMiddleware above a little cleaner + m.Use(middleware.EnvInit) + m.Use(AuthMiddleware) + m.Use(LoggerMiddleware) m.Get("/baz", func(c web.C, w http.ResponseWriter, r *http.Request) { if user, ok := c.Env["user"].(string); ok { w.Write([]byte("Hello " + user)) @@ -83,46 +74,41 @@ import ( ) /* -C is a per-request context object which is threaded through all compliant middleware -layers and to the final request handler. - -As an implementation detail, references to these structs are reused between -requests to reduce allocation churn, but the maps they contain are created fresh -on every request. If you are closing over a context (especially relevant for -middleware), you should not close over either the URLParams or Env objects, -instead accessing them through the context whenever they are required. +C is a request-local context object which is threaded through all compliant +middleware layers and given to the final request handler. */ type C struct { - // The parameters parsed by the mux from the URL itself. In most cases, - // will contain a map from programmer-specified identifiers to the - // strings that matched those identifiers, but if a unnamed regex - // capture is used, it will be assigned to the special identifiers "$1", - // "$2", etc. + // URLParams is a map of variables extracted from the URL (typically + // from the path portion) during routing. See the documentation for the + // URL Pattern you are using (or the documentation for ParsePattern for + // the case of standard pattern types) for more information about how + // variables are extracted and named. URLParams map[string]string - // A free-form environment, similar to Rack or PEP 333's environments. - // Middleware layers are encouraged to pass data to downstream layers - // and other handlers using this map, and are even more strongly - // encouraged to document and maybe namespace the keys they use. - Env map[string]interface{} + // Env is a free-form environment for storing request-local data. Keys + // may be arbitrary types that support equality, however package-private + // types with type-safe accessors provide a convenient way for packages + // to mediate access to their request-local data. + Env map[interface{}]interface{} } -// Handler is a superset of net/http's http.Handler, which also includes a -// mechanism for serving requests with a context. If your handler does not -// support the use of contexts, we encourage you to use http.Handler instead. +// Handler is similar to net/http's http.Handler, but also accepts a Goji +// context object. type Handler interface { - http.Handler ServeHTTPC(C, http.ResponseWriter, *http.Request) } -// HandlerFunc is like net/http's http.HandlerFunc, but supports a context -// object. Implements both http.Handler and web.Handler free of charge. +// HandlerFunc is similar to net/http's http.HandlerFunc, but supports a context +// object. Implements both http.Handler and web.Handler. type HandlerFunc func(C, http.ResponseWriter, *http.Request) +// ServeHTTP implements http.Handler, allowing HandlerFunc's to be used with +// net/http and other compliant routers. When used in this way, the underlying +// function will be passed an empty context. func (h HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { h(C{}, w, r) } -// ServeHTTPC wraps ServeHTTP with a context parameter. +// ServeHTTPC implements Handler. func (h HandlerFunc) ServeHTTPC(c C, w http.ResponseWriter, r *http.Request) { h(c, w, r) }