Merge remote-tracking branch 'da-z/patch-1' into chaining
diff --git a/.travis.yml b/.travis.yml
index b0cf782..814710d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,4 +3,5 @@
- 1.1
- 1.2
- 1.3
+ - 1.4
- tip
diff --git a/README.md b/README.md
index 25201e1..f7ca0a3 100644
--- a/README.md
+++ b/README.md
@@ -196,8 +196,9 @@
Or you could [just write your own](http://justinas.org/writing-http-middleware-in-go/),
it's very easy!
-Alternatively, you could try [a framework building upon HttpRouter](#web-frameworks-building-upon-httprouter).
+Alternatively, you could try [a framework building upon HttpRouter](#web-frameworks--co-based-on-httprouter).
+### Multi-domain / Sub-domains
Here is a quick example: Does your server serve multiple domains / hosts?
You want to use sub-domains?
Define a router per host!
@@ -235,7 +236,69 @@
}
```
-## Web Frameworks building upon HttpRouter
+### Basic Authentication
+Another quick example: Basic Authentification (RFC 2617) for handles:
+
+```go
+package main
+
+import (
+ "bytes"
+ "encoding/base64"
+ "fmt"
+ "github.com/julienschmidt/httprouter"
+ "net/http"
+ "log"
+ "strings"
+)
+
+func BasicAuth(h httprouter.Handle, user, pass []byte) httprouter.Handle {
+ return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+ const basicAuthPrefix string = "Basic "
+
+ // Get the Basic Authentication credentials
+ auth := r.Header.Get("Authorization")
+ if strings.HasPrefix(auth, basicAuthPrefix) {
+ // Check credentials
+ payload, err := base64.StdEncoding.DecodeString(auth[len(basicAuthPrefix):])
+ if err == nil {
+ pair := bytes.SplitN(payload, []byte(":"), 2)
+ if len(pair) == 2 && bytes.Equal(pair[0], user) && bytes.Equal(pair[1], pass) {
+ // Delegate request to the given handle
+ h(w, r, ps)
+ return
+ }
+ }
+ }
+
+ // Request Basic Authentication otherwise
+ w.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
+ http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+ }
+}
+
+func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+ fmt.Fprint(w, "Not protected!\n")
+}
+
+func Protected(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+ fmt.Fprint(w, "Protected!\n")
+}
+
+func main() {
+ user := []byte("gordon")
+ pass := []byte("secret!")
+
+ router := httprouter.New()
+ router.GET("/", Index)
+ router.GET("/protected/", BasicAuth(Protected, user, pass))
+
+ log.Fatal(http.ListenAndServe(":8080", router))
+}
+```
+
+## Web Frameworks & Co based on HttpRouter
If the HttpRouter is a bit too minimalistic for you, you might try one of the following more high-level 3rd-party web frameworks building upon the HttpRouter package:
* [Gin](https://github.com/gin-gonic/gin): Features a martini-like API with much better performance
* [Hikaru](https://github.com/najeira/hikaru): Supports standalone and Google AppEngine
+* [Hitch](https://github.com/nbio/hitch): Hitch ties httprouter, [httpcontext](https://github.com/nbio/httpcontext), and middleware up in a bow
diff --git a/router.go b/router.go
index 3f3d163..ac3d680 100644
--- a/router.go
+++ b/router.go
@@ -123,13 +123,21 @@
// handle is registered for it.
// First superfluous path elements like ../ or // are removed.
// Afterwards the router does a case-insensitive lookup of the cleaned path.
- // If a handle can be found for this route, the router makes a redirection
+ // If a handle can be found for this route, the router makes a redirection
// to the corrected path with status code 301 for GET requests and 307 for
// all other request methods.
// For example /FOO and /..//Foo could be redirected to /foo.
// RedirectTrailingSlash is independent of this option.
RedirectFixedPath bool
+ // If enabled, the router checks if another method is allowed for the
+ // current route, if the current request can not be routed.
+ // If this is the case, the request is answered with 'Method Not Allowed'
+ // and HTTP status code 405.
+ // If no other Method is allowed, the request is delegated to the NotFound
+ // handler.
+ HandleMethodNotAllowed bool
+
// Configurable http.HandlerFunc which is called when no matching route is
// found. If it is not set, http.NotFound is used.
NotFound http.HandlerFunc
@@ -149,8 +157,9 @@
// Path auto-correction, including trailing slashes, is enabled by default.
func New() *Router {
return &Router{
- RedirectTrailingSlash: true,
- RedirectFixedPath: true,
+ RedirectTrailingSlash: true,
+ RedirectFixedPath: true,
+ HandleMethodNotAllowed: true,
}
}
@@ -159,6 +168,11 @@
r.Handle("GET", path, handle)
}
+// HEAD is a shortcut for router.Handle("HEAD", path, handle)
+func (r *Router) HEAD(path string, handle Handle) {
+ r.Handle("HEAD", path, handle)
+}
+
// POST is a shortcut for router.Handle("POST", path, handle)
func (r *Router) POST(path string, handle Handle) {
r.Handle("POST", path, handle)
@@ -284,7 +298,7 @@
}
if tsr && r.RedirectTrailingSlash {
- if path[len(path)-1] == '/' {
+ if len(path) > 1 && path[len(path)-1] == '/' {
req.URL.Path = path[:len(path)-1]
} else {
req.URL.Path = path + "/"
@@ -308,6 +322,25 @@
}
}
+ // Handle 405
+ if r.HandleMethodNotAllowed {
+ for method := range r.trees {
+ // Skip the requested method - we already tried this one
+ if method == req.Method {
+ continue
+ }
+
+ handle, _, _ := r.trees[method].getValue(req.URL.Path)
+ if handle != nil {
+ http.Error(w,
+ http.StatusText(http.StatusMethodNotAllowed),
+ http.StatusMethodNotAllowed,
+ )
+ return
+ }
+ }
+ }
+
// Handle 404
if r.NotFound != nil {
r.NotFound(w, req)
diff --git a/router_test.go b/router_test.go
index ca59066..6292ba8 100644
--- a/router_test.go
+++ b/router_test.go
@@ -76,7 +76,7 @@
}
func TestRouterAPI(t *testing.T) {
- var get, post, put, patch, delete, handler, handlerFunc bool
+ var get, head, post, put, patch, delete, handler, handlerFunc bool
httpHandler := handlerStruct{&handler}
@@ -84,6 +84,9 @@
router.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) {
get = true
})
+ router.HEAD("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) {
+ head = true
+ })
router.POST("/POST", func(w http.ResponseWriter, r *http.Request, _ Params) {
post = true
})
@@ -109,6 +112,12 @@
t.Error("routing GET failed")
}
+ r, _ = http.NewRequest("HEAD", "/GET", nil)
+ router.ServeHTTP(w, r)
+ if !head {
+ t.Error("routing HEAD failed")
+ }
+
r, _ = http.NewRequest("POST", "/POST", nil)
router.ServeHTTP(w, r)
if !post {
@@ -156,12 +165,28 @@
}
}
+func TestRouterNotAllowed(t *testing.T) {
+ handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {}
+
+ router := New()
+ router.POST("/path", handlerFunc)
+
+ // Test not allowed
+ r, _ := http.NewRequest("GET", "/path", nil)
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, r)
+ if !(w.Code == http.StatusMethodNotAllowed) {
+ t.Errorf("NotAllowed handling route %s failed: Code=%d, Header=%v", w.Code, w.Header())
+ }
+}
+
func TestRouterNotFound(t *testing.T) {
handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {}
router := New()
router.GET("/path", handlerFunc)
router.GET("/dir/", handlerFunc)
+ router.GET("/", handlerFunc)
testRoutes := []struct {
route string
@@ -170,6 +195,7 @@
}{
{"/path/", 301, "map[Location:[/path]]"}, // TSR -/
{"/dir", 301, "map[Location:[/dir/]]"}, // TSR +/
+ {"", 301, "map[Location:[/]]"}, // TSR +/
{"/PATH", 301, "map[Location:[/path]]"}, // Fixed Case
{"/DIR/", 301, "map[Location:[/dir/]]"}, // Fixed Case
{"/PATH/", 301, "map[Location:[/path]]"}, // Fixed Case -/