├── .gitignore ├── BENCHMARK_RESULTS ├── LICENSE ├── README.md ├── cover.sh ├── error_test.go ├── invalid_setup_test.go ├── logger_middleware.go ├── logger_middleware_test.go ├── middleware_test.go ├── not_found_test.go ├── options_handler.go ├── options_handler_test.go ├── panic_handler.go ├── request.go ├── response_writer.go ├── response_writer_test.go ├── router_serve.go ├── router_setup.go ├── routing_test.go ├── show_errors_middleware.go ├── show_errors_middleware_test.go ├── speed_test.go ├── static_middleware.go ├── static_middleware_test.go ├── tree.go └── web_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.out -------------------------------------------------------------------------------- /BENCHMARK_RESULTS: -------------------------------------------------------------------------------- 1 | Result of running `go test speed_test.go -test.bench=.* -test.benchmem=true` on my 2.3 GHz Macbook Pro. 2 | 3 | 2014/08/30 commit 056ab3d12ad9e8120f7818454ee5c27c478e2b3d (minor tweaks) 4 | BenchmarkGocraftWeb_Simple 2000000 788 ns/op 347 B/op 7 allocs/op 5 | BenchmarkGocraftWeb_Route15 1000000 2329 ns/op 736 B/op 10 allocs/op 6 | BenchmarkGocraftWeb_Route75 1000000 2339 ns/op 735 B/op 10 allocs/op 7 | BenchmarkGocraftWeb_Route150 1000000 2331 ns/op 737 B/op 10 allocs/op 8 | BenchmarkGocraftWeb_Route300 1000000 2329 ns/op 736 B/op 10 allocs/op 9 | BenchmarkGocraftWeb_Route3000 1000000 2392 ns/op 738 B/op 10 allocs/op 10 | BenchmarkGocraftWeb_Middleware 200000 6533 ns/op 1690 B/op 23 allocs/op 11 | BenchmarkGocraftWeb_Generic 1000000 1428 ns/op 486 B/op 9 allocs/op 12 | BenchmarkGocraftWeb_Composite 200000 8018 ns/op 1851 B/op 23 allocs/op 13 | 14 | // 15 | // Everything below is via a slower Macbook Air (1.8 GHz): 16 | // 17 | 18 | 2013/12/15 commit a32b88c2a5e1df88a16cd909f800460ff79edbaf (playing with how dynamic handlers are invoked. middleware/composite seem to be mutually exclusive for 5k ns/op) 19 | BenchmarkGocraftWeb_Simple 1000000 1216 ns/op 333 B/op 7 allocs/op 20 | BenchmarkGocraftWeb_Route15 500000 3337 ns/op 612 B/op 9 allocs/op 21 | BenchmarkGocraftWeb_Route75 500000 3234 ns/op 612 B/op 9 allocs/op 22 | BenchmarkGocraftWeb_Route150 500000 3298 ns/op 612 B/op 9 allocs/op 23 | BenchmarkGocraftWeb_Route300 500000 3217 ns/op 612 B/op 9 allocs/op 24 | BenchmarkGocraftWeb_Route3000 500000 3478 ns/op 615 B/op 9 allocs/op 25 | BenchmarkGocraftWeb_Middleware 200000 14566 ns/op 779 B/op 16 allocs/op 26 | BenchmarkGocraftWeb_Generic 1000000 1885 ns/op 457 B/op 9 allocs/op 27 | BenchmarkGocraftWeb_Composite 200000 9750 ns/op 917 B/op 16 allocs/op 28 | 29 | 2013/12/15 commit 51a79f5dae0bc4847a5eb7cc8b44b6eeb70c13fc (fast generic action handlers. Don't call generic action handlers via reflection) 30 | BenchmarkGocraftWeb_Simple 1000000 1271 ns/op 333 B/op 7 allocs/op 31 | BenchmarkGocraftWeb_Route15 500000 2955 ns/op 633 B/op 9 allocs/op 32 | BenchmarkGocraftWeb_Route75 500000 2959 ns/op 633 B/op 9 allocs/op 33 | BenchmarkGocraftWeb_Route150 500000 2982 ns/op 633 B/op 9 allocs/op 34 | BenchmarkGocraftWeb_Route300 500000 3046 ns/op 633 B/op 9 allocs/op 35 | BenchmarkGocraftWeb_Route3000 500000 3459 ns/op 633 B/op 9 allocs/op 36 | BenchmarkGocraftWeb_Middleware 200000 9438 ns/op 779 B/op 16 allocs/op 37 | BenchmarkGocraftWeb_Generic 1000000 1933 ns/op 457 B/op 9 allocs/op 38 | BenchmarkGocraftWeb_Composite 100000 15030 ns/op 1072 B/op 17 allocs/op 39 | 40 | 2013/12/15 commit 0e41f71c706db24fd56979361bb5cfa19b4828e2 (fast generic middleware. Don't call generic middleware via reflection) 41 | BenchmarkGocraftWeb_Simple 1000000 2036 ns/op 366 B/op 8 allocs/op 42 | BenchmarkGocraftWeb_Route15 500000 3080 ns/op 713 B/op 10 allocs/op 43 | BenchmarkGocraftWeb_Route75 500000 3452 ns/op 713 B/op 10 allocs/op 44 | BenchmarkGocraftWeb_Route150 500000 3199 ns/op 713 B/op 10 allocs/op 45 | BenchmarkGocraftWeb_Route300 500000 3212 ns/op 713 B/op 10 allocs/op 46 | BenchmarkGocraftWeb_Route3000 500000 3667 ns/op 713 B/op 10 allocs/op 47 | BenchmarkGocraftWeb_Middleware 200000 8888 ns/op 862 B/op 17 allocs/op 48 | BenchmarkGocraftWeb_Generic 1000000 2853 ns/op 490 B/op 10 allocs/op 49 | BenchmarkGocraftWeb_Composite 200000 12352 ns/op 1152 B/op 18 allocs/op 50 | 51 | 2013/12/15 commit e75a372bb80dc50627730b52adfcbdc4a6a723f5 (optimization pass. reduce allocations. manual closure.) 52 | BenchmarkGocraftWeb_Simple 1000000 2042 ns/op 366 B/op 8 allocs/op 53 | BenchmarkGocraftWeb_Route15 500000 3203 ns/op 713 B/op 10 allocs/op 54 | BenchmarkGocraftWeb_Route75 500000 3237 ns/op 713 B/op 10 allocs/op 55 | BenchmarkGocraftWeb_Route150 500000 3231 ns/op 713 B/op 10 allocs/op 56 | BenchmarkGocraftWeb_Route300 500000 3263 ns/op 713 B/op 10 allocs/op 57 | BenchmarkGocraftWeb_Route3000 500000 3719 ns/op 713 B/op 10 allocs/op 58 | BenchmarkGocraftWeb_Middleware 200000 9148 ns/op 862 B/op 17 allocs/op 59 | BenchmarkGocraftWeb_Generic 200000 8229 ns/op 680 B/op 16 allocs/op 60 | BenchmarkGocraftWeb_Composite 200000 10038 ns/op 1152 B/op 18 allocs/op 61 | 62 | 2013/12/14 commit fb4b15ee25ce11e3ea583f38cecebec8979ebb12 (no functional changes; just revamping benches) 63 | BenchmarkGocraftWeb_Simple 1000000 2517 ns/op 472 B/op 16 allocs/op 64 | BenchmarkGocraftWeb_Route15 500000 3829 ns/op 859 B/op 20 allocs/op 65 | BenchmarkGocraftWeb_Route75 500000 3944 ns/op 859 B/op 20 allocs/op 66 | BenchmarkGocraftWeb_Route150 500000 3977 ns/op 859 B/op 20 allocs/op 67 | BenchmarkGocraftWeb_Route300 500000 3948 ns/op 859 B/op 20 allocs/op 68 | BenchmarkGocraftWeb_Route3000 500000 4492 ns/op 860 B/op 20 allocs/op 69 | BenchmarkGocraftWeb_Middleware 200000 11068 ns/op 1683 B/op 35 allocs/op 70 | BenchmarkGocraftWeb_Generic 200000 9336 ns/op 913 B/op 28 allocs/op 71 | BenchmarkGocraftWeb_Composite 200000 11572 ns/op 1890 B/op 34 allocs/op 72 | 73 | 2013/11/27 commit 0991ee88750eb6b054c715ef03cf8f9fba47c6e5 (after implementing regexp segment conditions) 74 | BenchmarkSimple 500000 2749 ns/op 537 B/op 16 allocs/op 75 | BenchmarkRouting 500000 3915 ns/op 843 B/op 19 allocs/op 76 | BenchmarkMiddleware 200000 9281 ns/op 897 B/op 27 allocs/op 77 | 78 | 2013/11/25 commit 49658cd6cd345d4e779a7033c6b5d1e3077f56f3 (baseline) 79 | BenchmarkSimple 500000 2760 ns/op 537 B/op 16 allocs/op 80 | BenchmarkRouting 500000 3896 ns/op 843 B/op 19 allocs/op 81 | BenchmarkMiddleware 200000 9177 ns/op 897 B/op 27 allocs/op 82 | 83 | 84 | NOTES ON PROFILING: 85 | go test -bench=BenchmarkGocraftWeb_Composite -benchmem -memprofile mem.out 86 | go tool pprof web.test mem.out 87 | rm myprof* && go build -o myprof examples/prof/main.go && ./myprof && go tool pprof myprof myprof.out -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jonathan Novak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gocraft/web [](https://godoc.org/github.com/gocraft/web) 2 | 3 | 4 | gocraft/web is a Go mux and middleware package. We deal with casting and reflection so YOUR code can be statically typed. And we're fast. 5 | 6 | ## Getting Started 7 | From your GOPATH: 8 | 9 | ```bash 10 | go get github.com/gocraft/web 11 | ``` 12 | 13 | Add a file ```server.go``` - for instance, ```src/myapp/server.go``` 14 | 15 | ```go 16 | package main 17 | 18 | import ( 19 | "github.com/gocraft/web" 20 | "fmt" 21 | "net/http" 22 | "strings" 23 | ) 24 | 25 | type Context struct { 26 | HelloCount int 27 | } 28 | 29 | func (c *Context) SetHelloCount(rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) { 30 | c.HelloCount = 3 31 | next(rw, req) 32 | } 33 | 34 | func (c *Context) SayHello(rw web.ResponseWriter, req *web.Request) { 35 | fmt.Fprint(rw, strings.Repeat("Hello ", c.HelloCount), "World!") 36 | } 37 | 38 | func main() { 39 | router := web.New(Context{}). // Create your router 40 | Middleware(web.LoggerMiddleware). // Use some included middleware 41 | Middleware(web.ShowErrorsMiddleware). // ... 42 | Middleware((*Context).SetHelloCount). // Your own middleware! 43 | Get("/", (*Context).SayHello) // Add a route 44 | http.ListenAndServe("localhost:3000", router) // Start the server! 45 | } 46 | ``` 47 | 48 | Run the server. It will be available on ```localhost:3000```: 49 | 50 | ```bash 51 | go run src/myapp/server.go 52 | ``` 53 | 54 | ## Features 55 | * **Super fast and scalable**. Added latency is from 3-9μs per request. Routing performance is O(log(N)) in the number of routes. 56 | * **Your own contexts**. Easily pass information between your middleware and handler with strong static typing. 57 | * **Easy and powerful routing**. Capture path variables. Validate path segments with regexps. Lovely API. 58 | * **Middleware**. Middleware can express almost any web-layer feature. We make it easy. 59 | * **Nested routers, contexts, and middleware**. Your app has an API, and admin area, and a logged out view. Each view needs different contexts and different middleware. We let you express this hierarchy naturally. 60 | * **Embrace Go's net/http package**. Start your server with http.ListenAndServe(), and work directly with http.ResponseWriter and http.Request. 61 | * **Minimal**. The core of gocraft/web is lightweight and minimal. Add optional functionality with our built-in middleware, or write your own middleware. 62 | 63 | ## Performance 64 | Performance is a first class concern. Every update to this package has its performance measured and tracked in [BENCHMARK_RESULTS](https://github.com/gocraft/web/blob/master/BENCHMARK_RESULTS). 65 | 66 | For minimal 'hello world' style apps, added latency is about 3μs. This grows to about 10μs for more complex apps (6 middleware functions, 3 levels of contexts, 150+ routes). 67 | 68 | 69 | One key design choice we've made is our choice of routing algorithm. Most competing libraries use simple O(N) iteration over all routes to find a match. This is fine if you have only a handful of routes, but starts to break down as your app gets bigger. We use a tree-based router which grows in complexity at O(log(N)). 70 | 71 | ## Application Structure 72 | 73 | ### Making your router 74 | The first thing you need to do is make a new router. Routers serve requests and execute middleware. 75 | 76 | ```go 77 | router := web.New(YourContext{}) 78 | ``` 79 | 80 | ### Your context 81 | Wait, what is YourContext{} and why do you need it? It can be any struct you want it to be. Here's an example of one: 82 | 83 | ```go 84 | type YourContext struct { 85 | User *User // Assumes you've defined a User type as well 86 | } 87 | ``` 88 | 89 | Your context can be empty or it can have various fields in it. The fields can be whatever you want - it's your type! When a new request comes into the router, we'll allocate an instance of this struct and pass it to your middleware and handlers. This allows, for instance, a SetUser middleware to set a User field that can be read in the handlers. 90 | 91 | ### Routes and handlers 92 | Once you have your router, you can add routes to it. Standard HTTP verbs are supported. 93 | 94 | ```go 95 | router := web.New(YourContext{}) 96 | router.Get("/users", (*YourContext).UsersList) 97 | router.Post("/users", (*YourContext).UsersCreate) 98 | router.Put("/users/:id", (*YourContext).UsersUpdate) 99 | router.Delete("/users/:id", (*YourContext).UsersDelete) 100 | router.Patch("/users/:id", (*YourContext).UsersUpdate) 101 | router.Get("/", (*YourContext).Root) 102 | ``` 103 | 104 | What is that funny ```(*YourContext).Root``` notation? It's called a method expression. It lets your handlers look like this: 105 | 106 | ```go 107 | func (c *YourContext) Root(rw web.ResponseWriter, req *web.Request) { 108 | if c.User != nil { 109 | fmt.Fprint(rw, "Hello,", c.User.Name) 110 | } else { 111 | fmt.Fprint(rw, "Hello, anonymous person") 112 | } 113 | } 114 | ``` 115 | 116 | All method expressions do is return a function that accepts the type as the first argument. So your handler can also look like this: 117 | 118 | ```go 119 | func Root(c *YourContext, rw web.ResponseWriter, req *web.Request) {} 120 | ``` 121 | 122 | Of course, if you don't need a context for a particular action, you can also do that: 123 | 124 | ```go 125 | func Root(rw web.ResponseWriter, req *web.Request) {} 126 | ``` 127 | 128 | Note that handlers always need to accept two input parameters: web.ResponseWriter, and *web.Request, both of which wrap the standard http.ResponseWriter and *http.Request, respectively. 129 | 130 | ### Middleware 131 | You can add middleware to a router: 132 | 133 | ```go 134 | router := web.New(YourContext{}) 135 | router.Middleware((*YourContext).UserRequired) 136 | // add routes, more middleware 137 | ``` 138 | 139 | This is what a middleware handler looks like: 140 | 141 | ```go 142 | func (c *YourContext) UserRequired(rw web.ResponseWriter, r *web.Request, next web.NextMiddlewareFunc) { 143 | user := userFromSession(r) // Pretend like this is defined. It reads a session cookie and returns a *User or nil. 144 | if user != nil { 145 | c.User = user 146 | next(rw, r) 147 | } else { 148 | rw.Header().Set("Location", "/") 149 | rw.WriteHeader(http.StatusMovedPermanently) 150 | // do NOT call next() 151 | } 152 | } 153 | ``` 154 | 155 | Some things to note about the above example: 156 | * We set fields in the context for future middleware / handlers to use. 157 | * We can call next(), or not. Not calling next() effectively stops the middleware stack. 158 | 159 | Of course, generic middleware without contexts is supported: 160 | 161 | ```go 162 | func GenericMiddleware(rw web.ResponseWriter, r *web.Request, next web.NextMiddlewareFunc) { 163 | // ... 164 | } 165 | ``` 166 | 167 | ### Nested routers 168 | Nested routers let you run different middleware and use different contexts for different parts of your app. Some common scenarios: 169 | * You want to run an AdminRequired middleware on all your admin routes, but not on API routes. Your context needs a CurrentAdmin field. 170 | * You want to run an OAuth middleware on your API routes. Your context needs an AccessToken field. 171 | * You want to run session handling middleware on ALL your routes. Your context needs a Session field. 172 | 173 | Let's implement that. Your contexts would look like this: 174 | 175 | ```go 176 | type Context struct { 177 | Session map[string]string 178 | } 179 | 180 | type AdminContext struct { 181 | *Context 182 | CurrentAdmin *User 183 | } 184 | 185 | type ApiContext struct { 186 | *Context 187 | AccessToken string 188 | } 189 | ``` 190 | 191 | Note that we embed a pointer to the parent context in each subcontext. This is required. 192 | 193 | Now that we have our contexts, let's create our routers: 194 | 195 | ```go 196 | rootRouter := web.New(Context{}) 197 | rootRouter.Middleware((*Context).LoadSession) 198 | 199 | apiRouter := rootRouter.Subrouter(ApiContext{}, "/api") 200 | apiRouter.Middleware((*ApiContext).OAuth) 201 | apiRouter.Get("/tickets", (*ApiContext).TicketsIndex) 202 | 203 | adminRouter := rootRouter.Subrouter(AdminContext{}, "/admin") 204 | adminRouter.Middleware((*AdminContext).AdminRequired) 205 | 206 | // Given the path namesapce for this router is "/admin", the full path of this route is "/admin/reports" 207 | adminRouter.Get("/reports", (*AdminContext).Reports) 208 | ``` 209 | 210 | Note that each time we make a subrouter, we need to supply the context as well as a path namespace. The context CAN be the same as the parent context, and the namespace CAN just be "/" for no namespace. 211 | 212 | ### Request lifecycle 213 | The following is a detailed account of the request lifecycle: 214 | 215 | 1. A request comes in. Yay! (follow along in ```router_serve.go``` if you'd like) 216 | 2. Wrap the default Go http.ResponseWriter and http.Request in a web.ResponseWriter and web.Request, respectively (via structure embedding). 217 | 3. Allocate a new root context. This context is passed into your root middleware. 218 | 4. Execute middleware on the root router. We do this before we find a route! 219 | 5. After all of the root router's middleware is executed, we'll run a 'virtual' routing middleware that determines the target route. 220 | * If the there's no route found, we'll execute the NotFound handler if supplied. Otherwise, we'll write a 404 response and start unwinding the root middlware. 221 | 6. Now that we have a target route, we can allocate the context tree of the target router. 222 | 7. Start executing middleware on the nested middleware leading up to the final router/route. 223 | 8. After all middleware is executed, we'll run another 'virtual' middleware that invokes the final handler corresponding to the target route. 224 | 9. Unwind all middleware calls (if there's any code after next() in the middleware, obviously that's going to run at some point). 225 | 226 | ### Capturing path params; regexp conditions 227 | You can capture path variables like this: 228 | 229 | ```go 230 | router.Get("/suggestions/:suggestion_id/comments/:comment_id") 231 | ``` 232 | 233 | In your handler, you can access them like this: 234 | 235 | ```go 236 | func (c *YourContext) Root(rw web.ResponseWriter, req *web.Request) { 237 | fmt.Fprint(rw, "Suggestion ID:", req.PathParams["suggestion_id"]) 238 | fmt.Fprint(rw, "Comment ID:", req.PathParams["comment_id"]) 239 | } 240 | ``` 241 | 242 | You can also validate the format of your path params with a regexp. For instance, to ensure the 'ids' start with a digit: 243 | 244 | ```go 245 | router.Get("/suggestions/:suggestion_id:\\d.*/comments/:comment_id:\\d.*") 246 | ``` 247 | 248 | You can match any route past a certain point like this: 249 | 250 | ```go 251 | router.Get("/suggestions/:suggestion_id/comments/:comment_id/:*") 252 | ``` 253 | 254 | The path params will contain a “*” member with the rest of your path. It is illegal to add any more paths past the “*” path param, as it’s meant to match every path afterwards, in all cases. 255 | 256 | For Example: 257 | /suggestions/123/comments/321/foo/879/bar/834 258 | 259 | Elicits path params: 260 | * “suggestion_id”: 123, 261 | * “comment_id”: 321, 262 | * “*”: “foo/879/bar/834” 263 | 264 | 265 | One thing you CANNOT currently do is use regexps outside of a path segment. For instance, optional path segments are not supported - you would have to define multiple routes that both point to the same handler. This design decision was made to enable efficient routing. 266 | 267 | ### Not Found handlers 268 | If a route isn't found, by default we'll return a 404 status and render the text "Not Found". 269 | 270 | You can supply a custom NotFound handler on your root router: 271 | 272 | ```go 273 | router.NotFound((*Context).NotFound) 274 | ``` 275 | 276 | Your handler can optionally accept a pointer to the root context. NotFound handlers look like this: 277 | 278 | ```go 279 | func (c *Context) NotFound(rw web.ResponseWriter, r *web.Request) { 280 | rw.WriteHeader(http.StatusNotFound) // You probably want to return 404. But you can also redirect or do whatever you want. 281 | fmt.Fprintf(rw, "My Not Found") // Render you own HTML or something! 282 | } 283 | ``` 284 | 285 | ### OPTIONS handlers 286 | If an [OPTIONS request](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing#Preflight_example) is made and routes with other methods are found for the requested path, then by default we'll return an empty response with an appropriate `Access-Control-Allow-Methods` header. 287 | 288 | You can supply a custom OPTIONS handler on your root router: 289 | 290 | ```go 291 | router.OptionsHandler((*Context).OptionsHandler) 292 | ``` 293 | 294 | Your handler can optionally accept a pointer to the root context. OPTIONS handlers look like this: 295 | 296 | ```go 297 | func (c *Context) OptionsHandler(rw web.ResponseWriter, r *web.Request, methods []string) { 298 | rw.Header().Add("Access-Control-Allow-Methods", strings.Join(methods, ", ")) 299 | rw.Header().Add("Access-Control-Allow-Origin", "*") 300 | } 301 | ``` 302 | 303 | ### Error handlers 304 | By default, if there's a panic in middleware or a handler, we'll return a 500 status and render the text "Application Error". 305 | 306 | If you use the included middleware ```web.ShowErrorsMiddleware```, a panic will result in a pretty backtrace being rendered in HTML. This is great for development. 307 | 308 | You can also supply a custom Error handler on any router (not just the root router): 309 | 310 | ```go 311 | router.Error((*Context).Error) 312 | ``` 313 | 314 | Your handler can optionally accept a pointer to its corresponding context. Error handlers look like this: 315 | 316 | ```go 317 | func (c *Context) Error(rw web.ResponseWriter, r *web.Request, err interface{}) { 318 | rw.WriteHeader(http.StatusInternalServerError) 319 | fmt.Fprint(w, "Error", err) 320 | } 321 | ``` 322 | 323 | ### Included middleware 324 | We ship with three basic pieces of middleware: a logger, an exception printer, and a static file server. To use them: 325 | 326 | ```go 327 | router := web.New(Context{}) 328 | router.Middleware(web.LoggerMiddleware). 329 | Middleware(web.ShowErrorsMiddleware) 330 | 331 | // The static middleware serves files. Examples: 332 | // "GET /" will serve an index file at pwd/public/index.html 333 | // "GET /robots.txt" will serve the file at pwd/public/robots.txt 334 | // "GET /images/foo.gif" will serve the file at pwd/public/images/foo.gif 335 | currentRoot, _ := os.Getwd() 336 | router.Middleware(web.StaticMiddleware(path.Join(currentRoot, "public"), web.StaticOption{IndexFile: "index.html"})) 337 | ``` 338 | 339 | NOTE: You might not want to use web.ShowErrorsMiddleware in production. You can easily do something like this: 340 | ```go 341 | router := web.New(Context{}) 342 | router.Middleware(web.LoggerMiddleware) 343 | if MyEnvironment == "development" { 344 | router.Middleware(web.ShowErrorsMiddleware) 345 | } 346 | // ... 347 | ``` 348 | 349 | ### Starting your server 350 | Since web.Router implements http.Handler (eg, ServeHTTP(ResponseWriter, *Request)), you can easily plug it in to the standard Go http machinery: 351 | 352 | ```go 353 | router := web.New(Context{}) 354 | // ... Add routes and such. 355 | http.ListenAndServe("localhost:8080", router) 356 | ``` 357 | 358 | ### Rendering responses 359 | So now you routed a request to a handler. You have a web.ResponseWriter (http.ResponseWriter) and web.Request (http.Request). Now what? 360 | 361 | ```go 362 | // You can print to the ResponseWriter! 363 | fmt.Fprintf(rw, "I'm a web page!") 364 | ``` 365 | 366 | This is currently where the implementation of this library stops. I recommend you read the documentation of [net/http](http://golang.org/pkg/net/http/). 367 | 368 | ## Extra Middlware 369 | This package is going to keep the built-in middlware simple and lean. Extra middleware can be found across the web: 370 | * [https://github.com/corneldamian/json-binding](https://github.com/corneldamian/json-binding) - mapping JSON request into a struct and response to json 371 | 372 | If you'd like me to link to your middleware, let me know with a pull request to this README. 373 | 374 | ## gocraft 375 | 376 | gocraft offers a toolkit for building web apps. Currently these packages are available: 377 | 378 | * [gocraft/web](https://github.com/gocraft/web) - Go Router + Middleware. Your Contexts. 379 | * [gocraft/dbr](https://github.com/gocraft/dbr) - Additions to Go's database/sql for super fast performance and convenience. 380 | * [gocraft/health](https://github.com/gocraft/health) - Instrument your web apps with logging and metrics. 381 | * [gocraft/work](https://github.com/gocraft/work) - Process background jobs in Go. 382 | 383 | These packages were developed by the [engineering team](https://eng.uservoice.com) at [UserVoice](https://www.uservoice.com) and currently power much of its infrastructure and tech stack. 384 | 385 | ## Thanks & Authors 386 | I use code/got inspiration from these excellent libraries: 387 | * [Revel](https://github.com/robfig/revel) - pathtree routing. 388 | * [Traffic](https://github.com/pilu/traffic) - inspiration, show errors middleware. 389 | * [Martini](https://github.com/codegangsta/martini) - static file serving. 390 | * [gorilla/mux](http://www.gorillatoolkit.org/pkg/mux) - inspiration. 391 | 392 | Authors: 393 | * Jonathan Novak -- [https://github.com/cypriss](https://github.com/cypriss) 394 | * Sponsored by [UserVoice](https://eng.uservoice.com) 395 | -------------------------------------------------------------------------------- /cover.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | go test -covermode=count -coverprofile=count.out . 4 | go tool cover -html=count.out 5 | go tool cover -func=count.out -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/stretchr/testify/assert" 7 | "log" 8 | "net/http" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func ErrorHandlerWithNoContext(w ResponseWriter, r *Request, err interface{}) { 14 | w.WriteHeader(http.StatusInternalServerError) 15 | fmt.Fprintf(w, "Contextless Error") 16 | } 17 | 18 | func TestNoErrorHandler(t *testing.T) { 19 | router := New(Context{}) 20 | router.Get("/action", (*Context).ErrorAction) 21 | 22 | admin := router.Subrouter(AdminContext{}, "/admin") 23 | admin.Get("/action", (*AdminContext).ErrorAction) 24 | 25 | rw, req := newTestRequest("GET", "/action") 26 | router.ServeHTTP(rw, req) 27 | assertResponse(t, rw, "Application Error", http.StatusInternalServerError) 28 | 29 | rw, req = newTestRequest("GET", "/admin/action") 30 | router.ServeHTTP(rw, req) 31 | assertResponse(t, rw, "Application Error", http.StatusInternalServerError) 32 | } 33 | 34 | func TestHandlerOnRoot(t *testing.T) { 35 | router := New(Context{}) 36 | router.Error((*Context).ErrorHandler) 37 | router.Get("/action", (*Context).ErrorAction) 38 | 39 | admin := router.Subrouter(AdminContext{}, "/admin") 40 | admin.Get("/action", (*AdminContext).ErrorAction) 41 | 42 | rw, req := newTestRequest("GET", "/action") 43 | router.ServeHTTP(rw, req) 44 | assertResponse(t, rw, "My Error", http.StatusInternalServerError) 45 | 46 | rw, req = newTestRequest("GET", "/admin/action") 47 | router.ServeHTTP(rw, req) 48 | assertResponse(t, rw, "My Error", http.StatusInternalServerError) 49 | } 50 | 51 | func TestContextlessError(t *testing.T) { 52 | router := New(Context{}) 53 | router.Error(ErrorHandlerWithNoContext) 54 | router.Get("/action", (*Context).ErrorAction) 55 | 56 | admin := router.Subrouter(AdminContext{}, "/admin") 57 | admin.Get("/action", (*AdminContext).ErrorAction) 58 | 59 | rw, req := newTestRequest("GET", "/action") 60 | router.ServeHTTP(rw, req) 61 | assert.Equal(t, http.StatusInternalServerError, rw.Code) 62 | assertResponse(t, rw, "Contextless Error", http.StatusInternalServerError) 63 | 64 | rw, req = newTestRequest("GET", "/admin/action") 65 | router.ServeHTTP(rw, req) 66 | assertResponse(t, rw, "Contextless Error", http.StatusInternalServerError) 67 | } 68 | 69 | func TestMultipleErrorHandlers(t *testing.T) { 70 | router := New(Context{}) 71 | router.Error((*Context).ErrorHandler) 72 | router.Get("/action", (*Context).ErrorAction) 73 | 74 | admin := router.Subrouter(AdminContext{}, "/admin") 75 | admin.Error((*AdminContext).ErrorHandler) 76 | admin.Get("/action", (*AdminContext).ErrorAction) 77 | 78 | rw, req := newTestRequest("GET", "/action") 79 | router.ServeHTTP(rw, req) 80 | assertResponse(t, rw, "My Error", http.StatusInternalServerError) 81 | 82 | rw, req = newTestRequest("GET", "/admin/action") 83 | router.ServeHTTP(rw, req) 84 | assertResponse(t, rw, "Admin Error", http.StatusInternalServerError) 85 | } 86 | 87 | func TestMultipleErrorHandlers2(t *testing.T) { 88 | router := New(Context{}) 89 | router.Get("/action", (*Context).ErrorAction) 90 | 91 | admin := router.Subrouter(AdminContext{}, "/admin") 92 | admin.Error((*AdminContext).ErrorHandler) 93 | admin.Get("/action", (*AdminContext).ErrorAction) 94 | 95 | api := router.Subrouter(APIContext{}, "/api") 96 | api.Error((*APIContext).ErrorHandler) 97 | api.Get("/action", (*APIContext).ErrorAction) 98 | 99 | rw, req := newTestRequest("GET", "/action") 100 | router.ServeHTTP(rw, req) 101 | assertResponse(t, rw, "Application Error", http.StatusInternalServerError) 102 | 103 | rw, req = newTestRequest("GET", "/admin/action") 104 | router.ServeHTTP(rw, req) 105 | assertResponse(t, rw, "Admin Error", http.StatusInternalServerError) 106 | 107 | rw, req = newTestRequest("GET", "/api/action") 108 | router.ServeHTTP(rw, req) 109 | assertResponse(t, rw, "Api Error", http.StatusInternalServerError) 110 | } 111 | 112 | func TestRootMiddlewarePanic(t *testing.T) { 113 | router := New(Context{}) 114 | router.Middleware((*Context).ErrorMiddleware) 115 | router.Error((*Context).ErrorHandler) 116 | admin := router.Subrouter(AdminContext{}, "/admin") 117 | admin.Error((*AdminContext).ErrorHandler) 118 | admin.Get("/action", (*AdminContext).ErrorAction) 119 | 120 | rw, req := newTestRequest("GET", "/admin/action") 121 | router.ServeHTTP(rw, req) 122 | assertResponse(t, rw, "My Error", 500) 123 | } 124 | 125 | func TestNonRootMiddlewarePanic(t *testing.T) { 126 | router := New(Context{}) 127 | router.Error((*Context).ErrorHandler) 128 | admin := router.Subrouter(AdminContext{}, "/admin") 129 | admin.Middleware((*AdminContext).ErrorMiddleware) 130 | admin.Error((*AdminContext).ErrorHandler) 131 | admin.Get("/action", (*AdminContext).ErrorAction) 132 | 133 | rw, req := newTestRequest("GET", "/admin/action") 134 | router.ServeHTTP(rw, req) 135 | assertResponse(t, rw, "Admin Error", 500) 136 | } 137 | 138 | func TestPanicLogging(t *testing.T) { 139 | var buf bytes.Buffer 140 | 141 | // Set the panichandler to our own time, then set it back after the test is done: 142 | oldHandler := PanicHandler 143 | PanicHandler = logPanicReporter{ 144 | log: log.New(&buf, "", 0), 145 | } 146 | defer func() { 147 | PanicHandler = oldHandler 148 | }() 149 | 150 | router := New(Context{}) 151 | router.Get("/action", (*Context).ErrorAction) 152 | 153 | rw, req := newTestRequest("GET", "/action") 154 | router.ServeHTTP(rw, req) 155 | assertResponse(t, rw, "Application Error", 500) 156 | 157 | if !strings.HasPrefix(buf.String(), "PANIC") { 158 | t.Error("Expected to have our PanicHandler be logged to.") 159 | } 160 | } 161 | 162 | func TestConsistentContext(t *testing.T) { 163 | router := New(Context{}) 164 | router.Error((*Context).ErrorHandler) 165 | admin := router.Subrouter(Context{}, "/admin") 166 | admin.Error((*Context).ErrorHandlerSecondary) 167 | admin.Get("/foo", (*Context).ErrorAction) 168 | 169 | rw, req := newTestRequest("GET", "/admin/foo") 170 | router.ServeHTTP(rw, req) 171 | assertResponse(t, rw, "My Secondary Error", 500) 172 | } 173 | -------------------------------------------------------------------------------- /invalid_setup_test.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func (c *Context) InvalidHandler() {} 9 | func (c *Context) InvalidHandler2(w ResponseWriter, r *Request) string { return "" } 10 | func (c *Context) InvalidHandler3(w ResponseWriter, r ResponseWriter) {} 11 | 12 | type invalidSubcontext struct{} 13 | 14 | func (c *invalidSubcontext) Handler(w ResponseWriter, r *Request) {} 15 | 16 | type invalidSubcontext2 struct { 17 | *invalidSubcontext 18 | } 19 | 20 | func TestInvalidContext(t *testing.T) { 21 | assert.Panics(t, func() { 22 | New(1) 23 | }) 24 | 25 | assert.Panics(t, func() { 26 | router := New(Context{}) 27 | router.Subrouter(invalidSubcontext{}, "") 28 | }) 29 | 30 | assert.Panics(t, func() { 31 | router := New(Context{}) 32 | router.Subrouter(invalidSubcontext2{}, "") 33 | }) 34 | } 35 | 36 | func TestInvalidHandler(t *testing.T) { 37 | router := New(Context{}) 38 | 39 | assert.Panics(t, func() { 40 | router.Get("/action", 1) 41 | }) 42 | 43 | assert.Panics(t, func() { 44 | router.Get("/action", (*Context).InvalidHandler) 45 | }) 46 | 47 | // Returns a string: 48 | assert.Panics(t, func() { 49 | router.Get("/action", (*Context).InvalidHandler2) 50 | }) 51 | 52 | // Two writer inputs: 53 | assert.Panics(t, func() { 54 | router.Get("/action", (*Context).InvalidHandler3) 55 | }) 56 | 57 | // Wrong context type: 58 | assert.Panics(t, func() { 59 | router.Get("/action", (*invalidSubcontext).Handler) 60 | }) 61 | 62 | // 63 | } 64 | 65 | func TestInvalidMiddleware(t *testing.T) { 66 | router := New(Context{}) 67 | 68 | assert.Panics(t, func() { 69 | router.Middleware((*Context).InvalidHandler) 70 | }) 71 | } 72 | 73 | func TestInvalidNotFound(t *testing.T) { 74 | router := New(Context{}) 75 | 76 | assert.Panics(t, func() { 77 | router.NotFound((*Context).InvalidHandler) 78 | }) 79 | 80 | // Valid handler not on main router: 81 | subrouter := router.Subrouter(Context{}, "") 82 | assert.Panics(t, func() { 83 | subrouter.NotFound((*Context).A) 84 | }) 85 | } 86 | 87 | func TestInvalidError(t *testing.T) { 88 | router := New(Context{}) 89 | 90 | assert.Panics(t, func() { 91 | router.Error((*Context).InvalidHandler) 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /logger_middleware.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "time" 7 | ) 8 | 9 | // Logger can be set to your own logger. Logger only applies to the LoggerMiddleware. 10 | var Logger = log.New(os.Stdout, "", 0) 11 | 12 | // LoggerMiddleware is generic middleware that will log requests to Logger (by default, Stdout). 13 | func LoggerMiddleware(rw ResponseWriter, req *Request, next NextMiddlewareFunc) { 14 | startTime := time.Now() 15 | 16 | next(rw, req) 17 | 18 | duration := time.Since(startTime).Nanoseconds() 19 | var durationUnits string 20 | switch { 21 | case duration > 2000000: 22 | durationUnits = "ms" 23 | duration /= 1000000 24 | case duration > 1000: 25 | durationUnits = "μs" 26 | duration /= 1000 27 | default: 28 | durationUnits = "ns" 29 | } 30 | 31 | Logger.Printf("[%d %s] %d '%s'\n", duration, durationUnits, rw.StatusCode(), req.URL.Path) 32 | } 33 | -------------------------------------------------------------------------------- /logger_middleware_test.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "regexp" 7 | "testing" 8 | ) 9 | 10 | func TestLoggerMiddleware(t *testing.T) { 11 | var buf bytes.Buffer 12 | Logger = log.New(&buf, "", 0) 13 | 14 | router := New(Context{}) 15 | router.Middleware(LoggerMiddleware) 16 | router.Get("/action", (*Context).A) 17 | 18 | // Hit an action: 19 | rw, req := newTestRequest("GET", "/action") 20 | router.ServeHTTP(rw, req) 21 | assertResponse(t, rw, "context-A", 200) 22 | 23 | // Make sure our buf has something good: 24 | logRegexp := regexp.MustCompile("\\[\\d+ .{2}\\] 200 '/action'") 25 | if !logRegexp.MatchString(buf.String()) { 26 | t.Error("Got invalid log entry: ", buf.String()) 27 | } 28 | 29 | // Do a 404: 30 | buf.Reset() 31 | rw, req = newTestRequest("GET", "/wat") 32 | router.ServeHTTP(rw, req) 33 | assertResponse(t, rw, "Not Found", 404) 34 | 35 | // Make sure our buf has something good: 36 | logRegexpNotFound := regexp.MustCompile("\\[\\d+ .{2}\\] 404 '/wat'") 37 | if !logRegexpNotFound.MatchString(buf.String()) { 38 | t.Error("Got invalid log entry: ", buf.String()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /middleware_test.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func (c *Context) A(w ResponseWriter, r *Request) { 9 | fmt.Fprintf(w, "context-A") 10 | } 11 | 12 | func (c *Context) Z(w ResponseWriter, r *Request) { 13 | fmt.Fprintf(w, "context-Z") 14 | } 15 | 16 | func (c *AdminContext) B(w ResponseWriter, r *Request) { 17 | fmt.Fprintf(w, "admin-B") 18 | } 19 | 20 | func (c *APIContext) C(w ResponseWriter, r *Request) { 21 | fmt.Fprintf(w, "api-C") 22 | } 23 | 24 | func (c *TicketsContext) D(w ResponseWriter, r *Request) { 25 | fmt.Fprintf(w, "tickets-D") 26 | } 27 | 28 | func (c *Context) mwNoNext(w ResponseWriter, r *Request, next NextMiddlewareFunc) { 29 | fmt.Fprintf(w, "context-mw-NoNext ") 30 | } 31 | 32 | func (c *Context) mwAlpha(w ResponseWriter, r *Request, next NextMiddlewareFunc) { 33 | fmt.Fprintf(w, "context-mw-Alpha ") 34 | next(w, r) 35 | } 36 | 37 | func (c *Context) mwBeta(w ResponseWriter, r *Request, next NextMiddlewareFunc) { 38 | fmt.Fprintf(w, "context-mw-Beta ") 39 | next(w, r) 40 | } 41 | 42 | func (c *Context) mwGamma(w ResponseWriter, r *Request, next NextMiddlewareFunc) { 43 | fmt.Fprintf(w, "context-mw-Gamma ") 44 | next(w, r) 45 | } 46 | 47 | func (c *APIContext) mwDelta(w ResponseWriter, r *Request, next NextMiddlewareFunc) { 48 | fmt.Fprintf(w, "api-mw-Delta ") 49 | next(w, r) 50 | } 51 | 52 | func (c *AdminContext) mwEpsilon(w ResponseWriter, r *Request, next NextMiddlewareFunc) { 53 | fmt.Fprintf(w, "admin-mw-Epsilon ") 54 | next(w, r) 55 | } 56 | 57 | func (c *AdminContext) mwZeta(w ResponseWriter, r *Request, next NextMiddlewareFunc) { 58 | fmt.Fprintf(w, "admin-mw-Zeta ") 59 | next(w, r) 60 | } 61 | 62 | func (c *TicketsContext) mwEta(w ResponseWriter, r *Request, next NextMiddlewareFunc) { 63 | fmt.Fprintf(w, "tickets-mw-Eta ") 64 | next(w, r) 65 | } 66 | 67 | func mwGenricInterface(ctx interface{}, w ResponseWriter, r *Request, next NextMiddlewareFunc) { 68 | fmt.Fprintf(w, "context-mw-Interface ") 69 | next(w, r) 70 | } 71 | 72 | func TestFlatNoMiddleware(t *testing.T) { 73 | router := New(Context{}) 74 | router.Get("/action", (*Context).A) 75 | router.Get("/action_z", (*Context).Z) 76 | 77 | rw, req := newTestRequest("GET", "/action") 78 | router.ServeHTTP(rw, req) 79 | assertResponse(t, rw, "context-A", 200) 80 | 81 | rw, req = newTestRequest("GET", "/action_z") 82 | router.ServeHTTP(rw, req) 83 | assertResponse(t, rw, "context-Z", 200) 84 | } 85 | 86 | func TestFlatOneMiddleware(t *testing.T) { 87 | router := New(Context{}) 88 | router.Middleware((*Context).mwAlpha) 89 | router.Get("/action", (*Context).A) 90 | router.Get("/action_z", (*Context).Z) 91 | 92 | rw, req := newTestRequest("GET", "/action") 93 | router.ServeHTTP(rw, req) 94 | assertResponse(t, rw, "context-mw-Alpha context-A", 200) 95 | 96 | rw, req = newTestRequest("GET", "/action_z") 97 | router.ServeHTTP(rw, req) 98 | assertResponse(t, rw, "context-mw-Alpha context-Z", 200) 99 | } 100 | 101 | func TestFlatTwoMiddleware(t *testing.T) { 102 | router := New(Context{}) 103 | router.Middleware((*Context).mwAlpha) 104 | router.Middleware((*Context).mwBeta) 105 | router.Get("/action", (*Context).A) 106 | router.Get("/action_z", (*Context).Z) 107 | 108 | rw, req := newTestRequest("GET", "/action") 109 | router.ServeHTTP(rw, req) 110 | assertResponse(t, rw, "context-mw-Alpha context-mw-Beta context-A", 200) 111 | 112 | rw, req = newTestRequest("GET", "/action_z") 113 | router.ServeHTTP(rw, req) 114 | assertResponse(t, rw, "context-mw-Alpha context-mw-Beta context-Z", 200) 115 | } 116 | 117 | func TestDualTree(t *testing.T) { 118 | router := New(Context{}) 119 | router.Middleware((*Context).mwAlpha) 120 | router.Get("/action", (*Context).A) 121 | admin := router.Subrouter(AdminContext{}, "/admin") 122 | admin.Middleware((*AdminContext).mwEpsilon) 123 | admin.Get("/action", (*AdminContext).B) 124 | api := router.Subrouter(APIContext{}, "/api") 125 | api.Middleware((*APIContext).mwDelta) 126 | api.Get("/action", (*APIContext).C) 127 | 128 | rw, req := newTestRequest("GET", "/action") 129 | router.ServeHTTP(rw, req) 130 | assertResponse(t, rw, "context-mw-Alpha context-A", 200) 131 | 132 | rw, req = newTestRequest("GET", "/admin/action") 133 | router.ServeHTTP(rw, req) 134 | assertResponse(t, rw, "context-mw-Alpha admin-mw-Epsilon admin-B", 200) 135 | 136 | rw, req = newTestRequest("GET", "/api/action") 137 | router.ServeHTTP(rw, req) 138 | assertResponse(t, rw, "context-mw-Alpha api-mw-Delta api-C", 200) 139 | } 140 | 141 | func TestDualLeaningLeftTree(t *testing.T) { 142 | router := New(Context{}) 143 | router.Get("/action", (*Context).A) 144 | admin := router.Subrouter(AdminContext{}, "/admin") 145 | admin.Get("/action", (*AdminContext).B) 146 | api := router.Subrouter(APIContext{}, "/api") 147 | api.Middleware((*APIContext).mwDelta) 148 | api.Get("/action", (*APIContext).C) 149 | 150 | rw, req := newTestRequest("GET", "/action") 151 | router.ServeHTTP(rw, req) 152 | assertResponse(t, rw, "context-A", 200) 153 | 154 | rw, req = newTestRequest("GET", "/admin/action") 155 | router.ServeHTTP(rw, req) 156 | assertResponse(t, rw, "admin-B", 200) 157 | 158 | rw, req = newTestRequest("GET", "/api/action") 159 | router.ServeHTTP(rw, req) 160 | assertResponse(t, rw, "api-mw-Delta api-C", 200) 161 | } 162 | 163 | func TestTicketsA(t *testing.T) { 164 | router := New(Context{}) 165 | admin := router.Subrouter(AdminContext{}, "/admin") 166 | admin.Middleware((*AdminContext).mwEpsilon) 167 | tickets := admin.Subrouter(TicketsContext{}, "/tickets") 168 | tickets.Get("/action", (*TicketsContext).D) 169 | 170 | rw, req := newTestRequest("GET", "/admin/tickets/action") 171 | router.ServeHTTP(rw, req) 172 | assertResponse(t, rw, "admin-mw-Epsilon tickets-D", 200) 173 | } 174 | 175 | func TestTicketsB(t *testing.T) { 176 | router := New(Context{}) 177 | admin := router.Subrouter(AdminContext{}, "/admin") 178 | tickets := admin.Subrouter(TicketsContext{}, "/tickets") 179 | tickets.Middleware((*TicketsContext).mwEta) 180 | tickets.Get("/action", (*TicketsContext).D) 181 | 182 | rw, req := newTestRequest("GET", "/admin/tickets/action") 183 | router.ServeHTTP(rw, req) 184 | assertResponse(t, rw, "tickets-mw-Eta tickets-D", 200) 185 | } 186 | 187 | func TestTicketsC(t *testing.T) { 188 | router := New(Context{}) 189 | router.Middleware((*Context).mwAlpha) 190 | admin := router.Subrouter(AdminContext{}, "/admin") 191 | tickets := admin.Subrouter(TicketsContext{}, "/tickets") 192 | tickets.Get("/action", (*TicketsContext).D) 193 | 194 | rw, req := newTestRequest("GET", "/admin/tickets/action") 195 | router.ServeHTTP(rw, req) 196 | assertResponse(t, rw, "context-mw-Alpha tickets-D", 200) 197 | } 198 | 199 | func TestTicketsD(t *testing.T) { 200 | router := New(Context{}) 201 | router.Middleware((*Context).mwAlpha) 202 | admin := router.Subrouter(AdminContext{}, "/admin") 203 | tickets := admin.Subrouter(TicketsContext{}, "/tickets") 204 | tickets.Middleware((*TicketsContext).mwEta) 205 | tickets.Get("/action", (*TicketsContext).D) 206 | 207 | rw, req := newTestRequest("GET", "/admin/tickets/action") 208 | router.ServeHTTP(rw, req) 209 | assertResponse(t, rw, "context-mw-Alpha tickets-mw-Eta tickets-D", 200) 210 | } 211 | 212 | func TestTicketsE(t *testing.T) { 213 | router := New(Context{}) 214 | router.Middleware((*Context).mwAlpha) 215 | router.Middleware((*Context).mwBeta) 216 | router.Middleware((*Context).mwGamma) 217 | admin := router.Subrouter(AdminContext{}, "/admin") 218 | admin.Middleware((*AdminContext).mwEpsilon) 219 | admin.Middleware((*AdminContext).mwZeta) 220 | tickets := admin.Subrouter(TicketsContext{}, "/tickets") 221 | tickets.Middleware((*TicketsContext).mwEta) 222 | tickets.Get("/action", (*TicketsContext).D) 223 | 224 | rw, req := newTestRequest("GET", "/admin/tickets/action") 225 | router.ServeHTTP(rw, req) 226 | assertResponse(t, rw, "context-mw-Alpha context-mw-Beta context-mw-Gamma admin-mw-Epsilon admin-mw-Zeta tickets-mw-Eta tickets-D", 200) 227 | } 228 | 229 | func TestNoNext(t *testing.T) { 230 | router := New(Context{}) 231 | router.Middleware((*Context).mwNoNext) 232 | router.Get("/action", (*Context).A) 233 | 234 | rw, req := newTestRequest("GET", "/action") 235 | router.ServeHTTP(rw, req) 236 | assertResponse(t, rw, "context-mw-NoNext", 200) 237 | } 238 | 239 | func TestSameContext(t *testing.T) { 240 | router := New(Context{}) 241 | router.Middleware((*Context).mwAlpha). 242 | Middleware((*Context).mwBeta) 243 | admin := router.Subrouter(Context{}, "/admin") 244 | admin.Middleware((*Context).mwGamma) 245 | admin.Get("/foo", (*Context).A) 246 | 247 | rw, req := newTestRequest("GET", "/admin/foo") 248 | router.ServeHTTP(rw, req) 249 | assertResponse(t, rw, "context-mw-Alpha context-mw-Beta context-mw-Gamma context-A", 200) 250 | } 251 | 252 | func TestSameNamespace(t *testing.T) { 253 | router := New(Context{}) 254 | admin := router.Subrouter(AdminContext{}, "/") 255 | admin.Get("/action", (*AdminContext).B) 256 | 257 | rw, req := newTestRequest("GET", "/action") 258 | router.ServeHTTP(rw, req) 259 | assertResponse(t, rw, "admin-B", 200) 260 | } 261 | 262 | func TestInterfaceMiddleware(t *testing.T) { 263 | router := New(Context{}) 264 | router.Middleware(mwGenricInterface) 265 | router.Get("/action", (*Context).A) 266 | 267 | rw, req := newTestRequest("GET", "/action") 268 | router.ServeHTTP(rw, req) 269 | assertResponse(t, rw, "context-mw-Interface context-A", 200) 270 | } 271 | -------------------------------------------------------------------------------- /not_found_test.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func MyNotFoundHandler(rw ResponseWriter, r *Request) { 10 | rw.WriteHeader(http.StatusNotFound) 11 | fmt.Fprintf(rw, "My Not Found") 12 | } 13 | 14 | func (c *Context) HandlerWithContext(rw ResponseWriter, r *Request) { 15 | rw.WriteHeader(http.StatusNotFound) 16 | fmt.Fprintf(rw, "My Not Found With Context") 17 | } 18 | 19 | func TestNoHandler(t *testing.T) { 20 | router := New(Context{}) 21 | 22 | rw, req := newTestRequest("GET", "/this_path_doesnt_exist") 23 | router.ServeHTTP(rw, req) 24 | assertResponse(t, rw, "Not Found", http.StatusNotFound) 25 | } 26 | 27 | func TestBadMethod(t *testing.T) { 28 | router := New(Context{}) 29 | 30 | rw, req := newTestRequest("POOP", "/this_path_doesnt_exist") 31 | router.ServeHTTP(rw, req) 32 | assertResponse(t, rw, "Not Found", http.StatusNotFound) 33 | } 34 | 35 | func TestWithHandler(t *testing.T) { 36 | router := New(Context{}) 37 | router.NotFound(MyNotFoundHandler) 38 | 39 | rw, req := newTestRequest("GET", "/this_path_doesnt_exist") 40 | router.ServeHTTP(rw, req) 41 | assertResponse(t, rw, "My Not Found", http.StatusNotFound) 42 | } 43 | 44 | func TestWithRootContext(t *testing.T) { 45 | router := New(Context{}) 46 | router.NotFound((*Context).HandlerWithContext) 47 | 48 | rw, req := newTestRequest("GET", "/this_path_doesnt_exist") 49 | router.ServeHTTP(rw, req) 50 | assertResponse(t, rw, "My Not Found With Context", http.StatusNotFound) 51 | } 52 | -------------------------------------------------------------------------------- /options_handler.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | func (r *Router) genericOptionsHandler(ctx reflect.Value, methods []string) func(rw ResponseWriter, req *Request) { 10 | return func(rw ResponseWriter, req *Request) { 11 | if r.optionsHandler.IsValid() { 12 | invoke(r.optionsHandler, ctx, []reflect.Value{reflect.ValueOf(rw), reflect.ValueOf(req), reflect.ValueOf(methods)}) 13 | } else { 14 | rw.Header().Add("Access-Control-Allow-Methods", strings.Join(methods, ", ")) 15 | rw.WriteHeader(http.StatusOK) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /options_handler_test.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestOptionsHandler(t *testing.T) { 10 | router := New(Context{}) 11 | 12 | sub := router.Subrouter(Context{}, "/sub") 13 | sub.Middleware(AccessControlMiddleware) 14 | sub.Get("/action", (*Context).A) 15 | sub.Put("/action", (*Context).A) 16 | 17 | rw, req := newTestRequest("OPTIONS", "/sub/action") 18 | router.ServeHTTP(rw, req) 19 | assertResponse(t, rw, "", 200) 20 | assert.Equal(t, "GET, PUT", rw.Header().Get("Access-Control-Allow-Methods")) 21 | assert.Equal(t, "*", rw.Header().Get("Access-Control-Allow-Origin")) 22 | 23 | rw, req = newTestRequest("GET", "/sub/action") 24 | router.ServeHTTP(rw, req) 25 | assert.Equal(t, "*", rw.Header().Get("Access-Control-Allow-Origin")) 26 | } 27 | 28 | func TestCustomOptionsHandler(t *testing.T) { 29 | router := New(Context{}) 30 | router.OptionsHandler((*Context).OptionsHandler) 31 | 32 | sub := router.Subrouter(Context{}, "/sub") 33 | sub.Middleware(AccessControlMiddleware) 34 | sub.Get("/action", (*Context).A) 35 | sub.Put("/action", (*Context).A) 36 | 37 | rw, req := newTestRequest("OPTIONS", "/sub/action") 38 | router.ServeHTTP(rw, req) 39 | assertResponse(t, rw, "", 200) 40 | assert.Equal(t, "GET, PUT", rw.Header().Get("Access-Control-Allow-Methods")) 41 | assert.Equal(t, "100", rw.Header().Get("Access-Control-Max-Age")) 42 | } 43 | 44 | func (c *Context) OptionsHandler(rw ResponseWriter, req *Request, methods []string) { 45 | rw.Header().Add("Access-Control-Allow-Methods", strings.Join(methods, ", ")) 46 | rw.Header().Add("Access-Control-Max-Age", "100") 47 | } 48 | 49 | func AccessControlMiddleware(rw ResponseWriter, req *Request, next NextMiddlewareFunc) { 50 | rw.Header().Add("Access-Control-Allow-Origin", "*") 51 | next(rw, req) 52 | } 53 | -------------------------------------------------------------------------------- /panic_handler.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | // PanicReporter can receive panics that happen when serving a request and report them to a log of some sort. 9 | type PanicReporter interface { 10 | // Panic is called with the URL of the request, the result of calling recover, and the stack. 11 | Panic(url string, err interface{}, stack string) 12 | } 13 | 14 | // PanicHandler will be logged to in panic conditions (eg, division by zero in an app handler). 15 | // Applications can set web.PanicHandler = your own logger, if they wish. 16 | // In terms of logging the requests / responses, see logger_middleware. That is a completely separate system. 17 | var PanicHandler = PanicReporter(logPanicReporter{ 18 | log: log.New(os.Stderr, "ERROR ", log.Ldate|log.Ltime|log.Lmicroseconds|log.Lshortfile), 19 | }) 20 | 21 | type logPanicReporter struct { 22 | log *log.Logger 23 | } 24 | 25 | func (l logPanicReporter) Panic(url string, err interface{}, stack string) { 26 | l.log.Printf("PANIC\nURL: %v\nERROR: %v\nSTACK:\n%s\n", url, err, stack) 27 | } 28 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | ) 7 | 8 | // Request wraps net/http's Request and gocraf/web specific fields. In particular, PathParams is used to access 9 | // captures params in your URL. A Request is sent to handlers on each request. 10 | type Request struct { 11 | *http.Request 12 | 13 | // PathParams exists if you have wildcards in your URL that you need to capture. 14 | // Eg, /users/:id/tickets/:ticket_id and /users/1/tickets/33 would yield the map {id: "3", ticket_id: "33"} 15 | PathParams map[string]string 16 | 17 | // The actual route that got invoked 18 | route *route 19 | 20 | rootContext reflect.Value // Root context. Set immediately. 21 | targetContext reflect.Value // The target context corresponding to the route. Not set until root middleware is done. 22 | } 23 | 24 | // IsRouted can be called from middleware to determine if the request has been routed yet. 25 | func (r *Request) IsRouted() bool { 26 | return r.route != nil 27 | } 28 | 29 | // RoutePath returns the routed path string. Eg, if a route was registered with 30 | // router.Get("/suggestions/:suggestion_id/comments", f), then RoutePath will return "/suggestions/:suggestion_id/comments". 31 | func (r *Request) RoutePath() string { 32 | if r.route != nil { 33 | return r.route.Path 34 | } 35 | return "" 36 | } 37 | -------------------------------------------------------------------------------- /response_writer.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | ) 9 | 10 | // ResponseWriter includes net/http's ResponseWriter and adds a StatusCode() method to obtain the written status code. 11 | // A ResponseWriter is sent to handlers on each request. 12 | type ResponseWriter interface { 13 | http.ResponseWriter 14 | http.Flusher 15 | http.Hijacker 16 | http.CloseNotifier 17 | 18 | // StatusCode returns the written status code, or 0 if none has been written yet. 19 | StatusCode() int 20 | // Written returns whether the header has been written yet. 21 | Written() bool 22 | // Size returns the size in bytes of the body written so far. 23 | Size() int 24 | } 25 | 26 | type appResponseWriter struct { 27 | http.ResponseWriter 28 | statusCode int 29 | size int 30 | } 31 | 32 | // Don't need this yet because we get it for free: 33 | func (w *appResponseWriter) Write(data []byte) (n int, err error) { 34 | if w.statusCode == 0 { 35 | w.statusCode = http.StatusOK 36 | } 37 | size, err := w.ResponseWriter.Write(data) 38 | w.size += size 39 | return size, err 40 | } 41 | 42 | func (w *appResponseWriter) WriteHeader(statusCode int) { 43 | w.statusCode = statusCode 44 | w.ResponseWriter.WriteHeader(statusCode) 45 | } 46 | 47 | func (w *appResponseWriter) StatusCode() int { 48 | return w.statusCode 49 | } 50 | 51 | func (w *appResponseWriter) Written() bool { 52 | return w.statusCode != 0 53 | } 54 | 55 | func (w *appResponseWriter) Size() int { 56 | return w.size 57 | } 58 | 59 | func (w *appResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 60 | hijacker, ok := w.ResponseWriter.(http.Hijacker) 61 | if !ok { 62 | return nil, nil, fmt.Errorf("the ResponseWriter doesn't support the Hijacker interface") 63 | } 64 | return hijacker.Hijack() 65 | } 66 | 67 | func (w *appResponseWriter) CloseNotify() <-chan bool { 68 | return w.ResponseWriter.(http.CloseNotifier).CloseNotify() 69 | } 70 | 71 | func (w *appResponseWriter) Flush() { 72 | flusher, ok := w.ResponseWriter.(http.Flusher) 73 | if ok { 74 | flusher.Flush() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /response_writer_test.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "bufio" 5 | "github.com/stretchr/testify/assert" 6 | "net" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | type hijackableResponse struct { 14 | Hijacked bool 15 | } 16 | 17 | func (h *hijackableResponse) Header() http.Header { 18 | return nil 19 | } 20 | func (h *hijackableResponse) Write(buf []byte) (int, error) { 21 | return 0, nil 22 | } 23 | func (h *hijackableResponse) WriteHeader(code int) { 24 | // no-op 25 | } 26 | func (h *hijackableResponse) Flush() { 27 | // no-op 28 | } 29 | func (h *hijackableResponse) Hijack() (net.Conn, *bufio.ReadWriter, error) { 30 | h.Hijacked = true 31 | return nil, nil, nil 32 | } 33 | func (h *hijackableResponse) CloseNotify() <-chan bool { 34 | return nil 35 | } 36 | 37 | type closeNotifyingRecorder struct { 38 | *httptest.ResponseRecorder 39 | closed chan bool 40 | } 41 | 42 | func (c *closeNotifyingRecorder) close() { 43 | c.closed <- true 44 | } 45 | 46 | func (c *closeNotifyingRecorder) CloseNotify() <-chan bool { 47 | return c.closed 48 | } 49 | 50 | func TestResponseWriterWrite(t *testing.T) { 51 | rec := httptest.NewRecorder() 52 | rw := ResponseWriter(&appResponseWriter{ResponseWriter: rec}) 53 | 54 | assert.Equal(t, rw.Written(), false) 55 | 56 | n, err := rw.Write([]byte("Hello world")) 57 | assert.Equal(t, n, 11) 58 | assert.NoError(t, err) 59 | 60 | assert.Equal(t, n, 11) 61 | assert.Equal(t, rec.Code, rw.StatusCode()) 62 | assert.Equal(t, rec.Code, http.StatusOK) 63 | assert.Equal(t, rec.Body.String(), "Hello world") 64 | assert.Equal(t, rw.Size(), 11) 65 | assert.Equal(t, rw.Written(), true) 66 | } 67 | 68 | func TestResponseWriterWriteHeader(t *testing.T) { 69 | rec := httptest.NewRecorder() 70 | rw := ResponseWriter(&appResponseWriter{ResponseWriter: rec}) 71 | 72 | rw.WriteHeader(http.StatusNotFound) 73 | assert.Equal(t, rec.Code, rw.StatusCode()) 74 | assert.Equal(t, rec.Code, http.StatusNotFound) 75 | } 76 | 77 | func TestResponseWriterHijack(t *testing.T) { 78 | hijackable := &hijackableResponse{} 79 | rw := ResponseWriter(&appResponseWriter{ResponseWriter: hijackable}) 80 | hijacker, ok := rw.(http.Hijacker) 81 | assert.True(t, ok) 82 | _, _, err := hijacker.Hijack() 83 | assert.NoError(t, err) 84 | assert.True(t, hijackable.Hijacked) 85 | } 86 | 87 | func TestResponseWriterHijackNotOK(t *testing.T) { 88 | rw := ResponseWriter(&appResponseWriter{ResponseWriter: httptest.NewRecorder()}) 89 | _, _, err := rw.Hijack() 90 | assert.Error(t, err) 91 | } 92 | 93 | func TestResponseWriterFlush(t *testing.T) { 94 | rw := ResponseWriter(&appResponseWriter{ResponseWriter: httptest.NewRecorder()}) 95 | rw.Flush() 96 | } 97 | 98 | func TestResponseWriterCloseNotify(t *testing.T) { 99 | rec := &closeNotifyingRecorder{ 100 | httptest.NewRecorder(), 101 | make(chan bool, 1), 102 | } 103 | rw := ResponseWriter(&appResponseWriter{ResponseWriter: rec}) 104 | closed := false 105 | notifier := rw.(http.CloseNotifier).CloseNotify() 106 | rec.close() 107 | select { 108 | case <-notifier: 109 | closed = true 110 | case <-time.After(time.Second): 111 | } 112 | assert.True(t, closed) 113 | } 114 | -------------------------------------------------------------------------------- /router_serve.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "reflect" 7 | "runtime" 8 | ) 9 | 10 | type middlewareClosure struct { 11 | appResponseWriter 12 | Request 13 | Routers []*Router 14 | Contexts []reflect.Value 15 | currentMiddlewareIndex int 16 | currentRouterIndex int 17 | currentMiddlewareLen int 18 | RootRouter *Router 19 | Next NextMiddlewareFunc 20 | } 21 | 22 | // This is the entry point for servering all requests. 23 | func (rootRouter *Router) ServeHTTP(rw http.ResponseWriter, r *http.Request) { 24 | 25 | // Manually create a closure. These variables are needed in middlewareStack. 26 | // The reason we put these here instead of in the middleware stack, is Go (as of 1.2) 27 | // creates a heap variable for each varaiable in the closure. To minimize that, we'll 28 | // just have one (closure *middlewareClosure). 29 | var closure middlewareClosure 30 | closure.Request.Request = r 31 | closure.appResponseWriter.ResponseWriter = rw 32 | closure.Routers = make([]*Router, 1, rootRouter.maxChildrenDepth) 33 | closure.Routers[0] = rootRouter 34 | closure.Contexts = make([]reflect.Value, 1, rootRouter.maxChildrenDepth) 35 | closure.Contexts[0] = reflect.New(rootRouter.contextType) 36 | closure.currentMiddlewareLen = len(rootRouter.middleware) 37 | closure.RootRouter = rootRouter 38 | closure.Request.rootContext = closure.Contexts[0] 39 | 40 | // Handle errors 41 | defer func() { 42 | if recovered := recover(); recovered != nil { 43 | rootRouter.handlePanic(&closure.appResponseWriter, &closure.Request, recovered) 44 | } 45 | }() 46 | 47 | next := middlewareStack(&closure) 48 | next(&closure.appResponseWriter, &closure.Request) 49 | } 50 | 51 | // This function executes the middleware stack. It does so creating/returning an anonymous function/closure. 52 | // This closure can be called multiple times (eg, next()). Each time it is called, the next middleware is called. 53 | // Each time a middleware is called, this 'next' function is passed into it, which will/might call it again. 54 | // There are two 'virtual' middlewares in this stack: the route choosing middleware, and the action invoking middleware. 55 | // The route choosing middleware is executed after all root middleware. It picks the route. 56 | // The action invoking middleware is executed after all middleware. It executes the final handler. 57 | func middlewareStack(closure *middlewareClosure) NextMiddlewareFunc { 58 | closure.Next = func(rw ResponseWriter, req *Request) { 59 | if closure.currentRouterIndex >= len(closure.Routers) { 60 | return 61 | } 62 | 63 | // Find middleware to invoke. The goal of this block is to set the middleware variable. If it can't be done, it will be nil. 64 | // Side effects of this block: 65 | // - set currentMiddlewareIndex, currentRouterIndex, currentMiddlewareLen 66 | // - calculate route, setting routers/contexts, and fields in req. 67 | var middleware *middlewareHandler 68 | if closure.currentMiddlewareIndex < closure.currentMiddlewareLen { 69 | middleware = closure.Routers[closure.currentRouterIndex].middleware[closure.currentMiddlewareIndex] 70 | } else { 71 | // We ran out of middleware on the current router 72 | if closure.currentRouterIndex == 0 { 73 | // If we're still on the root router, it's time to actually figure out what the route is. 74 | // Do so, and update the various variables. 75 | // We could also 404 at this point: if so, run NotFound handlers and return. 76 | theRoute, wildcardMap := calculateRoute(closure.RootRouter, req) 77 | 78 | if theRoute == nil && httpMethod(req.Method) == httpMethodOptions { 79 | optionsMethod := req.Header.Get("Access-Control-Request-Method") 80 | methods := make([]string, 0, len(httpMethods)) 81 | var lastLeaf *pathLeaf 82 | for _, method := range httpMethods { 83 | if method == httpMethodOptions { 84 | continue 85 | } 86 | tree := closure.RootRouter.root[method] 87 | leaf, wildcards := tree.Match(req.URL.Path) 88 | if leaf != nil { 89 | methods = append(methods, string(method)) 90 | lastLeaf = leaf 91 | if optionsMethod == string(method) { 92 | wildcardMap = wildcards 93 | } 94 | } 95 | } 96 | 97 | if len(methods) > 0 { 98 | handler := &actionHandler{Generic: true, GenericHandler: closure.RootRouter.genericOptionsHandler(closure.Contexts[0], methods)} 99 | theRoute = &route{Method: httpMethodOptions, Path: lastLeaf.route.Path, Router: lastLeaf.route.Router, Handler: handler} 100 | } 101 | } 102 | 103 | if theRoute == nil { 104 | if closure.RootRouter.notFoundHandler.IsValid() { 105 | invoke(closure.RootRouter.notFoundHandler, closure.Contexts[0], []reflect.Value{reflect.ValueOf(rw), reflect.ValueOf(req)}) 106 | } else { 107 | rw.WriteHeader(http.StatusNotFound) 108 | fmt.Fprintf(rw, DefaultNotFoundResponse) 109 | } 110 | return 111 | } 112 | 113 | closure.Routers = routersFor(theRoute, closure.Routers) 114 | closure.Contexts = contextsFor(closure.Contexts, closure.Routers) 115 | 116 | req.targetContext = closure.Contexts[len(closure.Contexts)-1] 117 | req.route = theRoute 118 | req.PathParams = wildcardMap 119 | } 120 | 121 | closure.currentMiddlewareIndex = 0 122 | closure.currentRouterIndex++ 123 | routersLen := len(closure.Routers) 124 | for closure.currentRouterIndex < routersLen { 125 | closure.currentMiddlewareLen = len(closure.Routers[closure.currentRouterIndex].middleware) 126 | if closure.currentMiddlewareLen > 0 { 127 | break 128 | } 129 | closure.currentRouterIndex++ 130 | } 131 | if closure.currentRouterIndex < routersLen { 132 | middleware = closure.Routers[closure.currentRouterIndex].middleware[closure.currentMiddlewareIndex] 133 | } else { 134 | // We're done! invoke the action 135 | handler := req.route.Handler 136 | if handler.Generic { 137 | handler.GenericHandler(rw, req) 138 | } else { 139 | handler.DynamicHandler.Call([]reflect.Value{closure.Contexts[len(closure.Contexts)-1], reflect.ValueOf(rw), reflect.ValueOf(req)}) 140 | } 141 | } 142 | } 143 | 144 | closure.currentMiddlewareIndex++ 145 | 146 | // Invoke middleware. 147 | if middleware != nil { 148 | middleware.invoke(closure.Contexts[closure.currentRouterIndex], rw, req, closure.Next) 149 | } 150 | } 151 | 152 | return closure.Next 153 | } 154 | 155 | func (mw *middlewareHandler) invoke(ctx reflect.Value, rw ResponseWriter, req *Request, next NextMiddlewareFunc) { 156 | if mw.Generic { 157 | mw.GenericMiddleware(rw, req, next) 158 | } else { 159 | mw.DynamicMiddleware.Call([]reflect.Value{ctx, reflect.ValueOf(rw), reflect.ValueOf(req), reflect.ValueOf(next)}) 160 | } 161 | } 162 | 163 | // Strange performance characteristics: this hurts benchmark scores. 164 | // func (ah *actionHandler) invoke(ctx reflect.Value, rw ResponseWriter, req *Request) { 165 | // if ah.Generic { 166 | // ah.GenericHandler(rw, req) 167 | // } else { 168 | // ah.DynamicHandler.Call([]reflect.Value{ctx, reflect.ValueOf(rw), reflect.ValueOf(req)}) 169 | // } 170 | // } 171 | 172 | func calculateRoute(rootRouter *Router, req *Request) (*route, map[string]string) { 173 | var leaf *pathLeaf 174 | var wildcardMap map[string]string 175 | method := httpMethod(req.Method) 176 | tree, ok := rootRouter.root[method] 177 | if ok { 178 | leaf, wildcardMap = tree.Match(req.URL.Path) 179 | } 180 | 181 | // If no match and this is a HEAD, route on GET. 182 | if leaf == nil && method == httpMethodHead { 183 | tree, ok := rootRouter.root[httpMethodGet] 184 | if ok { 185 | leaf, wildcardMap = tree.Match(req.URL.Path) 186 | } 187 | } 188 | 189 | if leaf == nil { 190 | return nil, nil 191 | } 192 | 193 | return leaf.route, wildcardMap 194 | } 195 | 196 | // given the route (and target router), return [root router, child router, ..., leaf route's router] 197 | // Use the memory in routers to store this information 198 | func routersFor(route *route, routers []*Router) []*Router { 199 | routers = routers[:0] 200 | curRouter := route.Router 201 | for curRouter != nil { 202 | routers = append(routers, curRouter) 203 | curRouter = curRouter.parent 204 | } 205 | 206 | // Reverse the slice 207 | s := 0 208 | e := len(routers) - 1 209 | for s < e { 210 | routers[s], routers[e] = routers[e], routers[s] 211 | s++ 212 | e-- 213 | } 214 | 215 | return routers 216 | } 217 | 218 | // contexts is initially filled with a single context for the root 219 | // routers is [root, child, ..., leaf] with at least 1 element 220 | // Returns [ctx for root, ... ctx for leaf] 221 | // NOTE: if two routers have the same contextType, then they'll share the exact same context. 222 | func contextsFor(contexts []reflect.Value, routers []*Router) []reflect.Value { 223 | routersLen := len(routers) 224 | 225 | for i := 1; i < routersLen; i++ { 226 | var ctx reflect.Value 227 | if routers[i].contextType == routers[i-1].contextType { 228 | ctx = contexts[i-1] 229 | } else { 230 | ctx = reflect.New(routers[i].contextType) 231 | // set the first field to the parent 232 | f := reflect.Indirect(ctx).Field(0) 233 | f.Set(contexts[i-1]) 234 | } 235 | contexts = append(contexts, ctx) 236 | } 237 | 238 | return contexts 239 | } 240 | 241 | // If there's a panic in the root middleware (so that we don't have a route/target), then invoke the root handler or default. 242 | // If there's a panic in other middleware, then invoke the target action's function. 243 | // If there's a panic in the action handler, then invoke the target action's function. 244 | func (rootRouter *Router) handlePanic(rw *appResponseWriter, req *Request, err interface{}) { 245 | var targetRouter *Router // This will be set to the router we want to use the errorHandler on. 246 | var context reflect.Value // this is the context of the target router 247 | 248 | if req.route == nil { 249 | targetRouter = rootRouter 250 | context = req.rootContext 251 | } else { 252 | targetRouter = req.route.Router 253 | context = req.targetContext 254 | 255 | for !targetRouter.errorHandler.IsValid() && targetRouter.parent != nil { 256 | targetRouter = targetRouter.parent 257 | 258 | // Need to set context to the next context, UNLESS the context is the same type. 259 | curContextStruct := reflect.Indirect(context) 260 | if targetRouter.contextType != curContextStruct.Type() { 261 | context = curContextStruct.Field(0) 262 | if reflect.Indirect(context).Type() != targetRouter.contextType { 263 | panic("bug: shouldn't get here") 264 | } 265 | } 266 | } 267 | } 268 | 269 | if targetRouter.errorHandler.IsValid() { 270 | invoke(targetRouter.errorHandler, context, []reflect.Value{reflect.ValueOf(rw), reflect.ValueOf(req), reflect.ValueOf(err)}) 271 | } else { 272 | http.Error(rw, DefaultPanicResponse, http.StatusInternalServerError) 273 | } 274 | 275 | const size = 4096 276 | stack := make([]byte, size) 277 | stack = stack[:runtime.Stack(stack, false)] 278 | 279 | PanicHandler.Panic(fmt.Sprint(req.URL), err, string(stack)) 280 | } 281 | 282 | func invoke(handler reflect.Value, ctx reflect.Value, values []reflect.Value) { 283 | handlerType := handler.Type() 284 | numIn := handlerType.NumIn() 285 | if numIn == len(values) { 286 | handler.Call(values) 287 | } else { 288 | values = append([]reflect.Value{ctx}, values...) 289 | handler.Call(values) 290 | } 291 | } 292 | 293 | // DefaultNotFoundResponse is the default text rendered when no route is found and no NotFound handlers are present. 294 | var DefaultNotFoundResponse = "Not Found" 295 | 296 | // DefaultPanicResponse is the default text rendered when a panic occurs and no Error handlers are present. 297 | var DefaultPanicResponse = "Application Error" 298 | -------------------------------------------------------------------------------- /router_setup.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | ) 7 | 8 | type httpMethod string 9 | 10 | const ( 11 | httpMethodGet = httpMethod("GET") 12 | httpMethodPost = httpMethod("POST") 13 | httpMethodPut = httpMethod("PUT") 14 | httpMethodDelete = httpMethod("DELETE") 15 | httpMethodPatch = httpMethod("PATCH") 16 | httpMethodHead = httpMethod("HEAD") 17 | httpMethodOptions = httpMethod("OPTIONS") 18 | ) 19 | 20 | var httpMethods = []httpMethod{httpMethodGet, httpMethodPost, httpMethodPut, httpMethodDelete, httpMethodPatch, httpMethodHead, httpMethodOptions} 21 | 22 | // Router implements net/http's Handler interface and is what you attach middleware, routes/handlers, and subrouters to. 23 | type Router struct { 24 | // Hierarchy: 25 | parent *Router // nil if root router. 26 | children []*Router 27 | maxChildrenDepth int 28 | 29 | // For each request we'll create one of these objects 30 | contextType reflect.Type 31 | 32 | // Eg, "/" or "/admin". Any routes added to this router will be prefixed with this. 33 | pathPrefix string 34 | 35 | // Routeset contents: 36 | middleware []*middlewareHandler 37 | routes []*route 38 | 39 | // The root pathnode is the same for a tree of Routers 40 | root map[httpMethod]*pathNode 41 | 42 | // This can can be set on any router. The target's ErrorHandler will be invoked if it exists 43 | errorHandler reflect.Value 44 | 45 | // This can only be set on the root handler, since by virtue of not finding a route, we don't have a target. 46 | // (That being said, in the future we could investigate namespace matches) 47 | notFoundHandler reflect.Value 48 | 49 | // This can only be set on the root handler, since by virtue of not finding a route, we don't have a target. 50 | optionsHandler reflect.Value 51 | } 52 | 53 | // NextMiddlewareFunc are functions passed into your middleware. To advance the middleware, call the function. 54 | // You should usually pass the existing ResponseWriter and *Request into the next middlware, but you can 55 | // chose to swap them if you want to modify values or capture things written to the ResponseWriter. 56 | type NextMiddlewareFunc func(ResponseWriter, *Request) 57 | 58 | // GenericMiddleware are middleware that doesn't have or need a context. General purpose middleware, such as 59 | // static file serving, has this signature. If your middlware doesn't need a context, you can use this 60 | // signature to get a small performance boost. 61 | type GenericMiddleware func(ResponseWriter, *Request, NextMiddlewareFunc) 62 | 63 | // GenericHandler are handlers that don't have or need a context. If your handler doesn't need a context, 64 | // you can use this signature to get a small performance boost. 65 | type GenericHandler func(ResponseWriter, *Request) 66 | 67 | type route struct { 68 | Router *Router 69 | Method httpMethod 70 | Path string 71 | Handler *actionHandler 72 | } 73 | 74 | type middlewareHandler struct { 75 | Generic bool 76 | DynamicMiddleware reflect.Value 77 | GenericMiddleware GenericMiddleware 78 | } 79 | 80 | type actionHandler struct { 81 | Generic bool 82 | DynamicHandler reflect.Value 83 | GenericHandler GenericHandler 84 | } 85 | 86 | var emptyInterfaceType = reflect.TypeOf((*interface{})(nil)).Elem() 87 | 88 | // New returns a new router with context type ctx. ctx should be a struct instance, 89 | // whose purpose is to communicate type information. On each request, an instance of this 90 | // context type will be automatically allocated and sent to handlers. 91 | func New(ctx interface{}) *Router { 92 | validateContext(ctx, nil) 93 | 94 | r := &Router{} 95 | r.contextType = reflect.TypeOf(ctx) 96 | r.pathPrefix = "/" 97 | r.maxChildrenDepth = 1 98 | r.root = make(map[httpMethod]*pathNode) 99 | for _, method := range httpMethods { 100 | r.root[method] = newPathNode() 101 | } 102 | return r 103 | } 104 | 105 | // NewWithPrefix returns a new router (see New) but each route will have an implicit prefix. 106 | // For instance, with pathPrefix = "/api/v2", all routes under this router will begin with "/api/v2". 107 | func NewWithPrefix(ctx interface{}, pathPrefix string) *Router { 108 | r := New(ctx) 109 | r.pathPrefix = pathPrefix 110 | 111 | return r 112 | } 113 | 114 | // Subrouter attaches a new subrouter to the specified router and returns it. 115 | // You can use the same context or pass a new one. If you pass a new one, it must 116 | // embed a pointer to the previous context in the first slot. You can also pass 117 | // a pathPrefix that each route will have. If "" is passed, then no path prefix is applied. 118 | func (r *Router) Subrouter(ctx interface{}, pathPrefix string) *Router { 119 | validateContext(ctx, r.contextType) 120 | 121 | // Create new router, link up hierarchy 122 | newRouter := &Router{parent: r} 123 | r.children = append(r.children, newRouter) 124 | 125 | // Increment maxChildrenDepth if this is the first child of the router 126 | if len(r.children) == 1 { 127 | curParent := r 128 | for curParent != nil { 129 | curParent.maxChildrenDepth = curParent.depth() 130 | curParent = curParent.parent 131 | } 132 | } 133 | 134 | newRouter.contextType = reflect.TypeOf(ctx) 135 | newRouter.pathPrefix = appendPath(r.pathPrefix, pathPrefix) 136 | newRouter.root = r.root 137 | 138 | return newRouter 139 | } 140 | 141 | // Middleware adds the specified middleware tot he router and returns the router. 142 | func (r *Router) Middleware(fn interface{}) *Router { 143 | vfn := reflect.ValueOf(fn) 144 | validateMiddleware(vfn, r.contextType) 145 | if vfn.Type().NumIn() == 3 { 146 | r.middleware = append(r.middleware, &middlewareHandler{Generic: true, GenericMiddleware: fn.(func(ResponseWriter, *Request, NextMiddlewareFunc))}) 147 | } else { 148 | r.middleware = append(r.middleware, &middlewareHandler{Generic: false, DynamicMiddleware: vfn}) 149 | } 150 | 151 | return r 152 | } 153 | 154 | // Error sets the specified function as the error handler (when panics happen) and returns the router. 155 | func (r *Router) Error(fn interface{}) *Router { 156 | vfn := reflect.ValueOf(fn) 157 | validateErrorHandler(vfn, r.contextType) 158 | r.errorHandler = vfn 159 | return r 160 | } 161 | 162 | // NotFound sets the specified function as the not-found handler (when no route matches) and returns the router. 163 | // Note that only the root router can have a NotFound handler. 164 | func (r *Router) NotFound(fn interface{}) *Router { 165 | if r.parent != nil { 166 | panic("You can only set a NotFoundHandler on the root router.") 167 | } 168 | vfn := reflect.ValueOf(fn) 169 | validateNotFoundHandler(vfn, r.contextType) 170 | r.notFoundHandler = vfn 171 | return r 172 | } 173 | 174 | // OptionsHandler sets the specified function as the options handler and returns the router. 175 | // Note that only the root router can have a OptionsHandler handler. 176 | func (r *Router) OptionsHandler(fn interface{}) *Router { 177 | if r.parent != nil { 178 | panic("You can only set an OptionsHandler on the root router.") 179 | } 180 | vfn := reflect.ValueOf(fn) 181 | validateOptionsHandler(vfn, r.contextType) 182 | r.optionsHandler = vfn 183 | return r 184 | } 185 | 186 | // Get will add a route to the router that matches on GET requests and the specified path. 187 | func (r *Router) Get(path string, fn interface{}) *Router { 188 | return r.addRoute(httpMethodGet, path, fn) 189 | } 190 | 191 | // Post will add a route to the router that matches on POST requests and the specified path. 192 | func (r *Router) Post(path string, fn interface{}) *Router { 193 | return r.addRoute(httpMethodPost, path, fn) 194 | } 195 | 196 | // Put will add a route to the router that matches on PUT requests and the specified path. 197 | func (r *Router) Put(path string, fn interface{}) *Router { 198 | return r.addRoute(httpMethodPut, path, fn) 199 | } 200 | 201 | // Delete will add a route to the router that matches on DELETE requests and the specified path. 202 | func (r *Router) Delete(path string, fn interface{}) *Router { 203 | return r.addRoute(httpMethodDelete, path, fn) 204 | } 205 | 206 | // Patch will add a route to the router that matches on PATCH requests and the specified path. 207 | func (r *Router) Patch(path string, fn interface{}) *Router { 208 | return r.addRoute(httpMethodPatch, path, fn) 209 | } 210 | 211 | // Head will add a route to the router that matches on HEAD requests and the specified path. 212 | func (r *Router) Head(path string, fn interface{}) *Router { 213 | return r.addRoute(httpMethodHead, path, fn) 214 | } 215 | 216 | // Options will add a route to the router that matches on OPTIONS requests and the specified path. 217 | func (r *Router) Options(path string, fn interface{}) *Router { 218 | return r.addRoute(httpMethodOptions, path, fn) 219 | } 220 | 221 | func (r *Router) addRoute(method httpMethod, path string, fn interface{}) *Router { 222 | vfn := reflect.ValueOf(fn) 223 | validateHandler(vfn, r.contextType) 224 | fullPath := appendPath(r.pathPrefix, path) 225 | route := &route{Method: method, Path: fullPath, Router: r} 226 | if vfn.Type().NumIn() == 2 { 227 | route.Handler = &actionHandler{Generic: true, GenericHandler: fn.(func(ResponseWriter, *Request))} 228 | } else { 229 | route.Handler = &actionHandler{Generic: false, DynamicHandler: vfn} 230 | } 231 | r.routes = append(r.routes, route) 232 | r.root[method].add(fullPath, route) 233 | return r 234 | } 235 | 236 | // Calculates the max child depth of the node. Leaves return 1. For Parent->Child, Parent is 2. 237 | func (r *Router) depth() int { 238 | max := 0 239 | for _, child := range r.children { 240 | childDepth := child.depth() 241 | if childDepth > max { 242 | max = childDepth 243 | } 244 | } 245 | return max + 1 246 | } 247 | 248 | // 249 | // Private methods: 250 | // 251 | 252 | // Panics unless validation is correct 253 | func validateContext(ctx interface{}, parentCtxType reflect.Type) { 254 | ctxType := reflect.TypeOf(ctx) 255 | 256 | if ctxType.Kind() != reflect.Struct { 257 | panic("web: Context needs to be a struct type") 258 | } 259 | 260 | if parentCtxType != nil && parentCtxType != ctxType { 261 | if ctxType.NumField() == 0 { 262 | panic("web: Context needs to have first field be a pointer to parent context") 263 | } 264 | 265 | fldType := ctxType.Field(0).Type 266 | 267 | // Ensure fld is a pointer to parentCtxType 268 | if fldType != reflect.PtrTo(parentCtxType) { 269 | panic("web: Context needs to have first field be a pointer to parent context") 270 | } 271 | } 272 | } 273 | 274 | // Panics unless fn is a proper handler wrt ctxType 275 | // eg, func(ctx *ctxType, writer, request) 276 | func validateHandler(vfn reflect.Value, ctxType reflect.Type) { 277 | var req *Request 278 | var resp func() ResponseWriter 279 | if !isValidHandler(vfn, ctxType, reflect.TypeOf(resp).Out(0), reflect.TypeOf(req)) { 280 | panic(instructiveMessage(vfn, "a handler", "handler", "rw web.ResponseWriter, req *web.Request", ctxType)) 281 | } 282 | } 283 | 284 | func validateErrorHandler(vfn reflect.Value, ctxType reflect.Type) { 285 | var req *Request 286 | var resp func() ResponseWriter 287 | if !isValidHandler(vfn, ctxType, reflect.TypeOf(resp).Out(0), reflect.TypeOf(req), emptyInterfaceType) { 288 | panic(instructiveMessage(vfn, "an error handler", "error handler", "rw web.ResponseWriter, req *web.Request, err interface{}", ctxType)) 289 | } 290 | } 291 | 292 | func validateNotFoundHandler(vfn reflect.Value, ctxType reflect.Type) { 293 | var req *Request 294 | var resp func() ResponseWriter 295 | if !isValidHandler(vfn, ctxType, reflect.TypeOf(resp).Out(0), reflect.TypeOf(req)) { 296 | panic(instructiveMessage(vfn, "a 'not found' handler", "not found handler", "rw web.ResponseWriter, req *web.Request", ctxType)) 297 | } 298 | } 299 | 300 | func validateOptionsHandler(vfn reflect.Value, ctxType reflect.Type) { 301 | var req *Request 302 | var resp func() ResponseWriter 303 | var methods []string 304 | if !isValidHandler(vfn, ctxType, reflect.TypeOf(resp).Out(0), reflect.TypeOf(req), reflect.TypeOf(methods)) { 305 | panic(instructiveMessage(vfn, "an 'options' handler", "options handler", "rw web.ResponseWriter, req *web.Request, methods []string", ctxType)) 306 | } 307 | } 308 | 309 | func validateMiddleware(vfn reflect.Value, ctxType reflect.Type) { 310 | var req *Request 311 | var resp func() ResponseWriter 312 | var n NextMiddlewareFunc 313 | if !isValidHandler(vfn, ctxType, reflect.TypeOf(resp).Out(0), reflect.TypeOf(req), reflect.TypeOf(n)) { 314 | panic(instructiveMessage(vfn, "middleware", "middleware", "rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc", ctxType)) 315 | } 316 | } 317 | 318 | // Ensures vfn is a function, that optionally takes a *ctxType as the first argument, followed by the specified types. Handlers have no return value. 319 | // Returns true if valid, false otherwise. 320 | func isValidHandler(vfn reflect.Value, ctxType reflect.Type, types ...reflect.Type) bool { 321 | fnType := vfn.Type() 322 | 323 | if fnType.Kind() != reflect.Func { 324 | return false 325 | } 326 | 327 | typesStartIdx := 0 328 | typesLen := len(types) 329 | numIn := fnType.NumIn() 330 | numOut := fnType.NumOut() 331 | 332 | if numOut != 0 { 333 | return false 334 | } 335 | 336 | if numIn == typesLen { 337 | // No context 338 | } else if numIn == (typesLen + 1) { 339 | // context, types 340 | firstArgType := fnType.In(0) 341 | if firstArgType != reflect.PtrTo(ctxType) && firstArgType != emptyInterfaceType { 342 | return false 343 | } 344 | typesStartIdx = 1 345 | } else { 346 | return false 347 | } 348 | 349 | for _, typeArg := range types { 350 | if fnType.In(typesStartIdx) != typeArg { 351 | return false 352 | } 353 | typesStartIdx++ 354 | } 355 | 356 | return true 357 | } 358 | 359 | // Since it's easy to pass the wrong method to a middleware/handler route, and since the user can't rely on static type checking since we use reflection, 360 | // lets be super helpful about what they did and what they need to do. 361 | // Arguments: 362 | // - vfn is the failed method 363 | // - addingType is for "You are adding {addingType} to a router...". Eg, "middleware" or "a handler" or "an error handler" 364 | // - yourType is for "Your {yourType} function can have...". Eg, "middleware" or "handler" or "error handler" 365 | // - args is like "rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc" 366 | // - NOTE: args can be calculated if you pass in each type. BUT, it doesn't have example argument name, so it has less copy/paste value. 367 | func instructiveMessage(vfn reflect.Value, addingType string, yourType string, args string, ctxType reflect.Type) string { 368 | // Get context type without package. 369 | ctxString := ctxType.String() 370 | splitted := strings.Split(ctxString, ".") 371 | if len(splitted) <= 1 { 372 | ctxString = splitted[0] 373 | } else { 374 | ctxString = splitted[1] 375 | } 376 | 377 | str := "\n" + strings.Repeat("*", 120) + "\n" 378 | str += "* You are adding " + addingType + " to a router with context type '" + ctxString + "'\n" 379 | str += "*\n*\n" 380 | str += "* Your " + yourType + " function can have one of these signatures:\n" 381 | str += "*\n" 382 | str += "* // If you don't need context:\n" 383 | str += "* func YourFunctionName(" + args + ")\n" 384 | str += "*\n" 385 | str += "* // If you want your " + yourType + " to accept a context:\n" 386 | str += "* func (c *" + ctxString + ") YourFunctionName(" + args + ") // or,\n" 387 | str += "* func YourFunctionName(c *" + ctxString + ", " + args + ")\n" 388 | str += "*\n" 389 | str += "* Unfortunately, your function has this signature: " + vfn.Type().String() + "\n" 390 | str += "*\n" 391 | str += strings.Repeat("*", 120) + "\n" 392 | 393 | return str 394 | } 395 | 396 | // Both rootPath/childPath are like "/" and "/users" 397 | // Assumption is that both are well-formed paths. 398 | // Returns a path without a trailing "/" unless the overall path is just "/" 399 | func appendPath(rootPath, childPath string) string { 400 | return strings.TrimRight(rootPath, "/") + "/" + strings.TrimLeft(childPath, "/") 401 | } 402 | -------------------------------------------------------------------------------- /routing_test.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "sort" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | // 14 | // We're going to test everything from an integration perspective b/c I don't want to expose 15 | // the tree.go guts. 16 | // 17 | 18 | type Ctx struct{} 19 | 20 | type routeTest struct { 21 | route string 22 | get string 23 | vars map[string]string 24 | } 25 | 26 | // Converts the map into a consistent, string-comparable string (to compare with another map) 27 | // Eg, stringifyMap({"foo": "bar"}) == stringifyMap({"foo": "bar"}) 28 | func stringifyMap(m map[string]string) string { 29 | if m == nil { 30 | return "" 31 | } 32 | keys := make([]string, 0, len(m)) 33 | for k := range m { 34 | keys = append(keys, k) 35 | } 36 | sort.Strings(keys) 37 | keysLenMinusOne := len(keys) - 1 38 | 39 | var b bytes.Buffer 40 | 41 | b.WriteString("[") 42 | for i, k := range keys { 43 | b.WriteString(k) 44 | b.WriteRune(':') 45 | b.WriteString(m[k]) 46 | 47 | if i != keysLenMinusOne { 48 | b.WriteRune(' ') 49 | } 50 | } 51 | b.WriteRune(']') 52 | 53 | return b.String() 54 | } 55 | 56 | func TestRoutes(t *testing.T) { 57 | router := New(Ctx{}) 58 | 59 | table := []routeTest{ 60 | { 61 | route: "/", 62 | get: "/", 63 | vars: nil, 64 | }, 65 | { 66 | route: "/api/action", 67 | get: "/api/action", 68 | vars: nil, 69 | }, 70 | { 71 | route: "/admin/action", 72 | get: "/admin/action", 73 | vars: nil, 74 | }, 75 | { 76 | route: "/admin/action.json", 77 | get: "/admin/action.json", 78 | vars: nil, 79 | }, 80 | { 81 | route: "/:api/action", 82 | get: "/poop/action", 83 | vars: map[string]string{"api": "poop"}, 84 | }, 85 | { 86 | route: "/api/:action", 87 | get: "/api/poop", 88 | vars: map[string]string{"action": "poop"}, 89 | }, 90 | { 91 | route: "/:seg1/:seg2/bob", 92 | get: "/a/b/bob", 93 | vars: map[string]string{"seg1": "a", "seg2": "b"}, 94 | }, 95 | { 96 | route: "/:seg1/:seg2/ron", 97 | get: "/c/d/ron", 98 | vars: map[string]string{"seg1": "c", "seg2": "d"}, 99 | }, 100 | { 101 | route: "/:seg1/:seg2/:seg3", 102 | get: "/c/d/wat", 103 | vars: map[string]string{"seg1": "c", "seg2": "d", "seg3": "wat"}, 104 | }, 105 | { 106 | route: "/:seg1/:seg2/ron/apple", 107 | get: "/c/d/ron/apple", 108 | vars: map[string]string{"seg1": "c", "seg2": "d"}, 109 | }, 110 | { 111 | route: "/:seg1/:seg2/ron/:apple", 112 | get: "/c/d/ron/orange", 113 | vars: map[string]string{"seg1": "c", "seg2": "d", "apple": "orange"}, 114 | }, 115 | { 116 | route: "/site2/:id:\\d+", 117 | get: "/site2/123", 118 | vars: map[string]string{"id": "123"}, 119 | }, 120 | { 121 | route: "/site2/:id:[a-z]+", 122 | get: "/site2/abc", 123 | vars: map[string]string{"id": "abc"}, 124 | }, 125 | { 126 | route: "/site2/:id:\\d[a-z]+", 127 | get: "/site2/1abc", 128 | vars: map[string]string{"id": "1abc"}, 129 | }, 130 | { 131 | route: "/site2/:id", 132 | get: "/site2/1abc1", 133 | vars: map[string]string{"id": "1abc1"}, 134 | }, 135 | { 136 | route: "/site2/:id:\\d+/other/:var:[A-Z]+", 137 | get: "/site2/123/other/OK", 138 | vars: map[string]string{"id": "123", "var": "OK"}, 139 | }, 140 | { 141 | route: "/site2/:id/:*", 142 | get: "/site2/1abc1/foo/bar/baz/boo", 143 | vars: map[string]string{"id": "1abc1", "*": "foo/bar/baz/boo"}, 144 | }, 145 | { 146 | route: "/site3/:id:\\d+/:*", 147 | get: "/site3/123/foo/bar/baz/boo", 148 | vars: map[string]string{"id": "123", "*": "foo/bar/baz/boo"}, 149 | }, 150 | { 151 | route: "/site3/:*", 152 | get: "/site3/foo/bar/baz/boo", 153 | vars: map[string]string{"*": "foo/bar/baz/boo"}, 154 | }, 155 | } 156 | 157 | // Create routes 158 | for _, rt := range table { 159 | //func: ensure closure is created per iteraction (it fails otherwise) 160 | func(exp string) { 161 | router.Get(rt.route, func(w ResponseWriter, r *Request) { 162 | w.Header().Set("X-VARS", stringifyMap(r.PathParams)) 163 | fmt.Fprintf(w, exp) 164 | }) 165 | }(rt.route) 166 | } 167 | 168 | // Execute them all: 169 | for _, rt := range table { 170 | recorder := httptest.NewRecorder() 171 | request, _ := http.NewRequest("GET", rt.get, nil) 172 | 173 | router.ServeHTTP(recorder, request) 174 | 175 | if recorder.Code != 200 { 176 | t.Error("Test:", rt, " Didn't get Code=200. Got Code=", recorder.Code) 177 | } 178 | body := strings.TrimSpace(string(recorder.Body.Bytes())) 179 | if body != rt.route { 180 | t.Error("Test:", rt, " Didn't get Body=", rt.route, ". Got Body=", body) 181 | } 182 | vars := recorder.Header().Get("X-VARS") 183 | if vars != stringifyMap(rt.vars) { 184 | t.Error("Test:", rt, " Didn't get Vars=", rt.vars, ". Got Vars=", vars) 185 | } 186 | } 187 | } 188 | 189 | func TestRoutesWithPrefix(t *testing.T) { 190 | router := NewWithPrefix(Ctx{}, "/v1") 191 | 192 | table := []routeTest{ 193 | { 194 | route: "/", 195 | get: "/v1/", 196 | vars: nil, 197 | }, 198 | { 199 | route: "/api/action", 200 | get: "/v1/api/action", 201 | vars: nil, 202 | }, 203 | { 204 | route: "/admin/action", 205 | get: "/v1/admin/action", 206 | vars: nil, 207 | }, 208 | { 209 | route: "/admin/action.json", 210 | get: "/v1/admin/action.json", 211 | vars: nil, 212 | }, 213 | { 214 | route: "/:api/action", 215 | get: "/v1/poop/action", 216 | vars: map[string]string{"api": "poop"}, 217 | }, 218 | { 219 | route: "/api/:action", 220 | get: "/v1/api/poop", 221 | vars: map[string]string{"action": "poop"}, 222 | }, 223 | { 224 | route: "/:seg1/:seg2/bob", 225 | get: "/v1/a/b/bob", 226 | vars: map[string]string{"seg1": "a", "seg2": "b"}, 227 | }, 228 | { 229 | route: "/:seg1/:seg2/ron", 230 | get: "/v1/c/d/ron", 231 | vars: map[string]string{"seg1": "c", "seg2": "d"}, 232 | }, 233 | { 234 | route: "/:seg1/:seg2/:seg3", 235 | get: "/v1/c/d/wat", 236 | vars: map[string]string{"seg1": "c", "seg2": "d", "seg3": "wat"}, 237 | }, 238 | { 239 | route: "/:seg1/:seg2/ron/apple", 240 | get: "/v1/c/d/ron/apple", 241 | vars: map[string]string{"seg1": "c", "seg2": "d"}, 242 | }, 243 | { 244 | route: "/:seg1/:seg2/ron/:apple", 245 | get: "/v1/c/d/ron/orange", 246 | vars: map[string]string{"seg1": "c", "seg2": "d", "apple": "orange"}, 247 | }, 248 | { 249 | route: "/site2/:id:\\d+", 250 | get: "/v1/site2/123", 251 | vars: map[string]string{"id": "123"}, 252 | }, 253 | { 254 | route: "/site2/:id:[a-z]+", 255 | get: "/v1/site2/abc", 256 | vars: map[string]string{"id": "abc"}, 257 | }, 258 | { 259 | route: "/site2/:id:\\d[a-z]+", 260 | get: "/v1/site2/1abc", 261 | vars: map[string]string{"id": "1abc"}, 262 | }, 263 | { 264 | route: "/site2/:id", 265 | get: "/v1/site2/1abc1", 266 | vars: map[string]string{"id": "1abc1"}, 267 | }, 268 | { 269 | route: "/site2/:id:\\d+/other/:var:[A-Z]+", 270 | get: "/v1/site2/123/other/OK", 271 | vars: map[string]string{"id": "123", "var": "OK"}, 272 | }, 273 | } 274 | 275 | // Create routes 276 | for _, rt := range table { 277 | // func: ensure closure is created per iteraction (it fails otherwise) 278 | func(exp string) { 279 | router.Get(rt.route, func(w ResponseWriter, r *Request) { 280 | w.Header().Set("X-VARS", stringifyMap(r.PathParams)) 281 | fmt.Fprintf(w, exp) 282 | }) 283 | }(rt.route) 284 | } 285 | 286 | // Execute them all: 287 | for _, rt := range table { 288 | recorder := httptest.NewRecorder() 289 | request, _ := http.NewRequest("GET", rt.get, nil) 290 | 291 | router.ServeHTTP(recorder, request) 292 | 293 | if recorder.Code != 200 { 294 | t.Error("Test:", rt, " Didn't get Code=200. Got Code=", recorder.Code) 295 | } 296 | body := strings.TrimSpace(string(recorder.Body.Bytes())) 297 | if body != rt.route { 298 | t.Error("Test:", rt, " Didn't get Body=", rt.route, ". Got Body=", body) 299 | } 300 | vars := recorder.Header().Get("X-VARS") 301 | if vars != stringifyMap(rt.vars) { 302 | t.Error("Test:", rt, " Didn't get Vars=", rt.vars, ". Got Vars=", vars) 303 | } 304 | } 305 | } 306 | 307 | func TestRouteVerbs(t *testing.T) { 308 | router := New(Context{}) 309 | router.Get("/a", func(w ResponseWriter, r *Request) { 310 | fmt.Fprintf(w, "GET") 311 | }) 312 | router.Put("/a", func(w ResponseWriter, r *Request) { 313 | fmt.Fprintf(w, "PUT") 314 | }) 315 | router.Post("/a", func(w ResponseWriter, r *Request) { 316 | fmt.Fprintf(w, "POST") 317 | }) 318 | router.Delete("/a", func(w ResponseWriter, r *Request) { 319 | fmt.Fprintf(w, "DELETE") 320 | }) 321 | router.Patch("/a", func(w ResponseWriter, r *Request) { 322 | fmt.Fprintf(w, "PATCH") 323 | }) 324 | router.Head("/a", func(w ResponseWriter, r *Request) { 325 | fmt.Fprintf(w, "HEAD") 326 | }) 327 | router.Options("/a", func(w ResponseWriter, r *Request) { 328 | fmt.Fprintf(w, "OPTIONS") 329 | }) 330 | 331 | for _, method := range httpMethods { 332 | method := string(method) 333 | 334 | recorder := httptest.NewRecorder() 335 | request, _ := http.NewRequest(method, "/a", nil) 336 | 337 | router.ServeHTTP(recorder, request) 338 | 339 | if recorder.Code != 200 { 340 | t.Error("Test:", method, " Didn't get Code=200. Got Code=", recorder.Code) 341 | } 342 | 343 | body := strings.TrimSpace(string(recorder.Body.Bytes())) 344 | if body != method { 345 | t.Error("Test:", method, " Didn't get Body=", method, ". Got Body=", body) 346 | } 347 | } 348 | } 349 | 350 | func TestRouteHead(t *testing.T) { 351 | router := New(Context{}) 352 | router.Get("/a", (*Context).A) 353 | 354 | rw, req := newTestRequest("GET", "/a") 355 | router.ServeHTTP(rw, req) 356 | assertResponse(t, rw, "context-A", 200) 357 | 358 | rw, req = newTestRequest("HEAD", "/a") 359 | router.ServeHTTP(rw, req) 360 | assertResponse(t, rw, "context-A", 200) 361 | } 362 | 363 | func TestIsRouted(t *testing.T) { 364 | router := New(Context{}) 365 | router.Middleware(func(w ResponseWriter, r *Request, next NextMiddlewareFunc) { 366 | if r.IsRouted() { 367 | t.Error("Shouldn't be routed yet but was.") 368 | } 369 | if r.RoutePath() != "" { 370 | t.Error("Shouldn't have a route path yet.") 371 | } 372 | next(w, r) 373 | if !r.IsRouted() { 374 | t.Error("Should have been routed but wasn't.") 375 | } 376 | }) 377 | subrouter := router.Subrouter(Context{}, "") 378 | subrouter.Middleware(func(w ResponseWriter, r *Request, next NextMiddlewareFunc) { 379 | if !r.IsRouted() { 380 | t.Error("Should have been routed but wasn't.") 381 | } 382 | next(w, r) 383 | if !r.IsRouted() { 384 | t.Error("Should have been routed but wasn't.") 385 | } 386 | }) 387 | subrouter.Get("/a", func(w ResponseWriter, r *Request) { 388 | fmt.Fprintf(w, r.RoutePath()) 389 | }) 390 | 391 | rw, req := newTestRequest("GET", "/a") 392 | router.ServeHTTP(rw, req) 393 | assertResponse(t, rw, "/a", 200) 394 | } 395 | -------------------------------------------------------------------------------- /show_errors_middleware.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "bufio" 5 | "html/template" 6 | "net/http" 7 | "os" 8 | "runtime" 9 | "strings" 10 | ) 11 | 12 | // ShowErrorsMiddleware will catch panics and render an HTML page with the stack trace. 13 | // This middleware should only be used in development. 14 | func ShowErrorsMiddleware(rw ResponseWriter, req *Request, next NextMiddlewareFunc) { 15 | defer func() { 16 | if err := recover(); err != nil { 17 | const size = 4096 18 | stack := make([]byte, size) 19 | stack = stack[:runtime.Stack(stack, false)] 20 | 21 | renderPrettyError(rw, req, err, stack) 22 | } 23 | }() 24 | 25 | next(rw, req) 26 | } 27 | 28 | func renderPrettyError(rw ResponseWriter, req *Request, err interface{}, stack []byte) { 29 | _, filePath, line, _ := runtime.Caller(5) 30 | 31 | data := map[string]interface{}{ 32 | "Error": err, 33 | "Stack": string(stack), 34 | "Params": req.URL.Query(), 35 | "Method": req.Method, 36 | "FilePath": filePath, 37 | "Line": line, 38 | "Lines": readErrorFileLines(filePath, line), 39 | } 40 | 41 | rw.Header().Set("Content-Type", "text/html") 42 | rw.WriteHeader(http.StatusInternalServerError) 43 | 44 | tpl := template.Must(template.New("ErrorPage").Parse(panicPageTpl)) 45 | tpl.Execute(rw, data) 46 | } 47 | 48 | func readErrorFileLines(filePath string, errorLine int) map[int]string { 49 | lines := make(map[int]string) 50 | 51 | file, err := os.Open(filePath) 52 | if err != nil { 53 | return lines 54 | } 55 | 56 | defer file.Close() 57 | 58 | reader := bufio.NewReader(file) 59 | currentLine := 0 60 | for { 61 | line, err := reader.ReadString('\n') 62 | if err != nil || currentLine > errorLine+5 { 63 | break 64 | } 65 | 66 | currentLine++ 67 | 68 | if currentLine >= errorLine-5 { 69 | lines[currentLine] = strings.Replace(line, "\n", "", -1) 70 | } 71 | } 72 | 73 | return lines 74 | } 75 | 76 | const panicPageTpl string = ` 77 | 78 |
79 |{{ .Error }}
158 |162 | In {{ .FilePath }}:{{ .Line }}
163 | 164 | 165 |
168 | {{ range $lineNumber, $line := .Lines }}{{ $lineNumber }}{{ end }}
169 | |
170 |
171 | {{ range $lineNumber, $line := .Lines }}{{ $line }}
172 | |
173 |
{{ .Stack }}177 |
Method: {{ .Method }}
179 |