Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

middleware.GetHead #248

Merged
merged 8 commits into from
Sep 1, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ language: go
go:
- 1.7.x
- 1.8.x
- 1.9.x
- tip

install:
Expand Down
39 changes: 21 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ included some useful/optional subpackages: [middleware](/middleware), [render](h

## Features

* **Lightweight** - cloc'd in <1000 LOC for the chi router
* **Lightweight** - cloc'd in ~1000 LOC for the chi router
* **Fast** - yes, see [benchmarks](#benchmarks)
* **100% compatible with net/http** - use any http or middleware pkg in the ecosystem that is also compatible with `net/http`
* **Designed for modular/composable APIs** - middlewares, inline middlewares, route groups and subrouter mounting
Expand Down Expand Up @@ -316,6 +316,7 @@ with `net/http` can be used with chi's mux.
| chi/middlware Handler | description |
|:----------------------|:---------------------------------------------------------------------------------
| Compress | Gzip compression for clients that accept compressed responses |
| GetHead | Automatically route undefined HEAD requests to GET handlers |
| Heartbeat | Monitoring endpoint to check the servers pulse |
| Logger | Logs the start and end of each request with the elapsed processing time |
| NoCache | Sets response headers to prevent clients from caching |
Expand Down Expand Up @@ -362,27 +363,29 @@ and..

The benchmark suite: https://github.com/pkieltyka/go-http-routing-benchmark

Comparison with other routers (as of June 21, 2017): https://gist.github.com/pkieltyka/c089f309abeb179cfc4deaa519956d8c
Results as of Aug 31, 2017 on Go 1.9.0

```shell
BenchmarkChi_Param 3000000 427 ns/op 304 B/op 2 allocs/op
BenchmarkChi_Param5 2000000 631 ns/op 304 B/op 2 allocs/op
BenchmarkChi_Param20 1000000 1343 ns/op 304 B/op 2 allocs/op
BenchmarkChi_ParamWrite 3000000 477 ns/op 304 B/op 2 allocs/op
BenchmarkChi_GithubStatic 3000000 452 ns/op 304 B/op 2 allocs/op
BenchmarkChi_GithubParam 2000000 616 ns/op 304 B/op 2 allocs/op
BenchmarkChi_GithubAll 10000 130637 ns/op 61716 B/op 406 allocs/op
BenchmarkChi_GPlusStatic 3000000 415 ns/op 304 B/op 2 allocs/op
BenchmarkChi_GPlusParam 3000000 465 ns/op 304 B/op 2 allocs/op
BenchmarkChi_GPlus2Params 3000000 548 ns/op 304 B/op 2 allocs/op
BenchmarkChi_GPlusAll 200000 6895 ns/op 3952 B/op 26 allocs/op
BenchmarkChi_ParseStatic 3000000 407 ns/op 304 B/op 2 allocs/op
BenchmarkChi_ParseParam 3000000 451 ns/op 304 B/op 2 allocs/op
BenchmarkChi_Parse2Params 3000000 504 ns/op 304 B/op 2 allocs/op
BenchmarkChi_ParseAll 100000 13221 ns/op 7904 B/op 52 allocs/op
BenchmarkChi_StaticAll 20000 84327 ns/op 47731 B/op 314 allocs/op
BenchmarkChi_Param 3000000 607 ns/op 432 B/op 3 allocs/op
BenchmarkChi_Param5 2000000 935 ns/op 432 B/op 3 allocs/op
BenchmarkChi_Param20 1000000 1944 ns/op 432 B/op 3 allocs/op
BenchmarkChi_ParamWrite 2000000 664 ns/op 432 B/op 3 allocs/op
BenchmarkChi_GithubStatic 2000000 627 ns/op 432 B/op 3 allocs/op
BenchmarkChi_GithubParam 2000000 847 ns/op 432 B/op 3 allocs/op
BenchmarkChi_GithubAll 10000 175556 ns/op 87700 B/op 609 allocs/op
BenchmarkChi_GPlusStatic 3000000 566 ns/op 432 B/op 3 allocs/op
BenchmarkChi_GPlusParam 2000000 652 ns/op 432 B/op 3 allocs/op
BenchmarkChi_GPlus2Params 2000000 767 ns/op 432 B/op 3 allocs/op
BenchmarkChi_GPlusAll 200000 9794 ns/op 5616 B/op 39 allocs/op
BenchmarkChi_ParseStatic 3000000 590 ns/op 432 B/op 3 allocs/op
BenchmarkChi_ParseParam 2000000 656 ns/op 432 B/op 3 allocs/op
BenchmarkChi_Parse2Params 2000000 715 ns/op 432 B/op 3 allocs/op
BenchmarkChi_ParseAll 100000 18045 ns/op 11232 B/op 78 allocs/op
BenchmarkChi_StaticAll 10000 108871 ns/op 67827 B/op 471 allocs/op
```

Comparison with other routers: https://gist.github.com/pkieltyka/c089f309abeb179cfc4deaa519956d8c

NOTE: the allocs in the benchmark above are from the calls to http.Request's
`WithContext(context.Context)` method that clones the http.Request, sets the `Context()`
on the duplicated (alloc'd) request and returns it the new request object. This is just
Expand Down
5 changes: 5 additions & 0 deletions chi.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ type Routes interface {

// Middlewares returns the list of middlewares in use by the router.
Middlewares() Middlewares

// Match searches the routing tree for a handler that matches
// the method/path - similar to routing a http request, but without
// executing the handler thereafter.
Match(rctx *Context, method, path string) bool
}

// Middlewares type is a slice of standard middleware handlers with methods
Expand Down
13 changes: 9 additions & 4 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ var (
// request context to track route patterns, URL parameters and
// an optional routing path.
type Context struct {
// Routing path override used during the route search.
Routes Routes

// Routing path/method override used during the route search.
// See Mux#routeHTTP method.
RoutePath string
RoutePath string
RouteMethod string

// Routing pattern stack throughout the lifecycle of the request,
// across all connected routers. It is a record of all matching
Expand Down Expand Up @@ -48,9 +51,11 @@ func NewRouteContext() *Context {
return &Context{}
}

// reset a routing context to its initial state.
func (x *Context) reset() {
// Reset a routing context to its initial state.
func (x *Context) Reset() {
x.Routes = nil
x.RoutePath = ""
x.RouteMethod = ""
x.RoutePatterns = x.RoutePatterns[:0]
x.URLParams.Keys = x.URLParams.Keys[:0]
x.URLParams.Values = x.URLParams.Values[:0]
Expand Down
38 changes: 38 additions & 0 deletions middleware/get_head.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package middleware

import (
"net/http"

"github.com/go-chi/chi"
)

func GetHead(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "HEAD" {
rctx := chi.RouteContext(r.Context())
routePath := rctx.RoutePath
if routePath == "" {
if r.URL.RawPath != "" {
routePath = r.URL.RawPath
} else {
routePath = r.URL.Path
}
}

// Temporary routing context to look-ahead before routing the request
tctx := chi.NewRouteContext()

// Attempt to find a HEAD handler for the routing path, if not found, traverse
// the router as through its a GET route, but proceed with the request
// with the HEAD method.
if !rctx.Routes.Match(tctx, "HEAD", routePath) {
rctx.RouteMethod = "GET"
rctx.RoutePath = routePath
next.ServeHTTP(w, r)
return
}
}

next.ServeHTTP(w, r)
})
}
66 changes: 66 additions & 0 deletions middleware/get_head_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package middleware

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/go-chi/chi"
)

func TestGetHead(t *testing.T) {
r := chi.NewRouter()
r.Use(GetHead)
r.Get("/hi", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Test", "yes")
w.Write([]byte("bye"))
})
r.Route("/articles", func(r chi.Router) {
r.Get("/{id}", func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
w.Header().Set("X-Article", id)
w.Write([]byte("article:" + id))
})
})
r.Route("/users", func(r chi.Router) {
r.Head("/{id}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-User", "-")
w.Write([]byte("user"))
})
r.Get("/{id}", func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
w.Header().Set("X-User", id)
w.Write([]byte("user:" + id))
})
})

ts := httptest.NewServer(r)
defer ts.Close()

if _, body := testRequest(t, ts, "GET", "/hi", nil); body != "bye" {
t.Fatalf(body)
}
if req, body := testRequest(t, ts, "HEAD", "/hi", nil); body != "" || req.Header.Get("X-Test") != "yes" {
t.Fatalf(body)
}
if _, body := testRequest(t, ts, "GET", "/", nil); body != "404 page not found\n" {
t.Fatalf(body)
}
if req, body := testRequest(t, ts, "HEAD", "/", nil); body != "" || req.StatusCode != 404 {
t.Fatalf(body)
}

if _, body := testRequest(t, ts, "GET", "/articles/5", nil); body != "article:5" {
t.Fatalf(body)
}
if req, body := testRequest(t, ts, "HEAD", "/articles/5", nil); body != "" || req.Header.Get("X-Article") != "5" {
t.Fatalf("expecting X-Article header '5' but got '%s'", req.Header.Get("X-Article"))
}

if _, body := testRequest(t, ts, "GET", "/users/1", nil); body != "user:1" {
t.Fatalf(body)
}
if req, body := testRequest(t, ts, "HEAD", "/users/1", nil); body != "" || req.Header.Get("X-User") != "-" {
t.Fatalf("expecting X-User header '-' but got '%s'", req.Header.Get("X-User"))
}
}
10 changes: 5 additions & 5 deletions middleware/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,27 @@ func init() {
testdataDir = path.Join(path.Dir(filename), "/../testdata")
}

func testRequest(t *testing.T, ts *httptest.Server, method, path string, body io.Reader) (int, string) {
func testRequest(t *testing.T, ts *httptest.Server, method, path string, body io.Reader) (*http.Response, string) {
req, err := http.NewRequest(method, ts.URL+path, body)
if err != nil {
t.Fatal(err)
return 0, ""
return nil, ""
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
return resp.StatusCode, ""
return nil, ""
}

respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
return resp.StatusCode, ""
return nil, ""
}
defer resp.Body.Close()

return resp.StatusCode, string(respBody)
return resp, string(respBody)
}

func assertNoError(t *testing.T, err error) {
Expand Down
10 changes: 5 additions & 5 deletions middleware/strip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,25 +114,25 @@ func TestRedirectSlashes(t *testing.T) {
ts := httptest.NewServer(r)
defer ts.Close()

if status, resp := testRequest(t, ts, "GET", "/", nil); resp != "root" && status != 200 {
if req, resp := testRequest(t, ts, "GET", "/", nil); resp != "root" && req.StatusCode != 200 {
t.Fatalf(resp)
}

// NOTE: the testRequest client will follow the redirection..
if status, resp := testRequest(t, ts, "GET", "//", nil); resp != "root" && status != 200 {
if req, resp := testRequest(t, ts, "GET", "//", nil); resp != "root" && req.StatusCode != 200 {
t.Fatalf(resp)
}

if status, resp := testRequest(t, ts, "GET", "/accounts/admin", nil); resp != "admin" && status != 200 {
if req, resp := testRequest(t, ts, "GET", "/accounts/admin", nil); resp != "admin" && req.StatusCode != 200 {
t.Fatalf(resp)
}

// NOTE: the testRequest client will follow the redirection..
if status, resp := testRequest(t, ts, "GET", "/accounts/admin/", nil); resp != "admin" && status != 200 {
if req, resp := testRequest(t, ts, "GET", "/accounts/admin/", nil); resp != "admin" && req.StatusCode != 200 {
t.Fatalf(resp)
}

if status, resp := testRequest(t, ts, "GET", "/nothing-here", nil); resp != "nothing here" && status != 200 {
if req, resp := testRequest(t, ts, "GET", "/nothing-here", nil); resp != "nothing here" && req.StatusCode != 200 {
t.Fatalf(resp)
}
}
Loading