Browse Source

Valid HTTP method discovery

Package web will now add a key to the environment when it fails to find a
valid route for the requested method, but when valid routes exist for other
methods.

This allows either the 404 handler or a sufficiently clever middleware layer to
provide support for OPTIONS automatically.
Carl Jackson 12 years ago
parent
commit
d992544806
4 changed files with 120 additions and 11 deletions
  1. +7
    -3
      web/pattern.go
  2. +1
    -1
      web/pattern_test.go
  3. +61
    -6
      web/router.go
  4. +51
    -1
      web/router_test.go

+ 7
- 3
web/pattern.go View File

@ -19,13 +19,13 @@ type regexpPattern struct {
func (p regexpPattern) Prefix() string { func (p regexpPattern) Prefix() string {
return p.prefix return p.prefix
} }
func (p regexpPattern) Match(r *http.Request, c *C) bool {
func (p regexpPattern) Match(r *http.Request, c *C, dryrun bool) bool {
matches := p.re.FindStringSubmatch(r.URL.Path) matches := p.re.FindStringSubmatch(r.URL.Path)
if matches == nil || len(matches) == 0 { if matches == nil || len(matches) == 0 {
return false return false
} }
if len(matches) == 1 {
if c == nil || dryrun || len(matches) == 1 {
return true return true
} }
@ -149,7 +149,7 @@ func (s stringPattern) Prefix() string {
return s.literals[0] return s.literals[0]
} }
func (s stringPattern) Match(r *http.Request, c *C) bool {
func (s stringPattern) Match(r *http.Request, c *C, dryrun bool) bool {
path := r.URL.Path path := r.URL.Path
matches := make([]string, len(s.pats)) matches := make([]string, len(s.pats))
for i := 0; i < len(s.pats); i++ { for i := 0; i < len(s.pats); i++ {
@ -181,6 +181,10 @@ func (s stringPattern) Match(r *http.Request, c *C) bool {
} }
} }
if c == nil || dryrun {
return true
}
if c.UrlParams == nil && len(matches) > 0 { if c.UrlParams == nil && len(matches) > 0 {
c.UrlParams = make(map[string]string, len(matches)-1) c.UrlParams = make(map[string]string, len(matches)-1)
} }


+ 1
- 1
web/pattern_test.go View File

@ -149,7 +149,7 @@ func TestPatterns(t *testing.T) {
} }
func runTest(t *testing.T, p Pattern, test patternTest) { func runTest(t *testing.T, p Pattern, test patternTest) {
result := p.Match(test.r, test.c)
result := p.Match(test.r, test.c, false)
if result != test.match { if result != test.match {
t.Errorf("Expected match(%v, %#v) to return %v", p, t.Errorf("Expected match(%v, %#v) to return %v", p,
test.r.URL.Path, test.match) test.r.URL.Path, test.match)


+ 61
- 6
web/router.go View File

@ -28,6 +28,8 @@ const (
mPOST | mPUT | mTRACE | mIDK mPOST | mPUT | mTRACE | mIDK
) )
const validMethods = "goji.web.validMethods"
type route struct { type route struct {
// Theory: most real world routes have a string prefix which is both // Theory: most real world routes have a string prefix which is both
// cheap(-ish) to test against and pretty selective. And, conveniently, // cheap(-ish) to test against and pretty selective. And, conveniently,
@ -61,8 +63,9 @@ type Pattern interface {
// Returns true if the request satisfies the pattern. This function is // Returns true if the request satisfies the pattern. This function is
// free to examine both the request and the context to make this // free to examine both the request and the context to make this
// decision. After it is certain that the request matches, this function // decision. After it is certain that the request matches, this function
// should mutate or create c.UrlParams if necessary.
Match(r *http.Request, c *C) bool
// should mutate or create c.UrlParams if necessary, unless dryrun is
// set.
Match(r *http.Request, c *C, dryrun bool) bool
} }
func parsePattern(p interface{}, isPrefix bool) Pattern { func parsePattern(p interface{}, isPrefix bool) Pattern {
@ -139,16 +142,64 @@ func httpMethod(mname string) method {
func (rt *router) route(c C, w http.ResponseWriter, r *http.Request) { func (rt *router) route(c C, w http.ResponseWriter, r *http.Request) {
m := httpMethod(r.Method) m := httpMethod(r.Method)
var methods method
for _, route := range rt.routes { for _, route := range rt.routes {
if route.method&m == 0 ||
!strings.HasPrefix(r.URL.Path, route.prefix) ||
!route.pattern.Match(r, &c) {
if !strings.HasPrefix(r.URL.Path, route.prefix) ||
!route.pattern.Match(r, &c, false) {
continue continue
} }
route.handler.ServeHTTPC(c, w, r)
if route.method&m != 0 {
route.handler.ServeHTTPC(c, w, r)
return
} else if route.pattern.Match(r, &c, true) {
methods |= route.method
}
}
if methods == 0 {
rt.notFound.ServeHTTPC(c, w, r)
return return
} }
// Oh god kill me now
var methodsList = make([]string, 0)
if methods&mCONNECT != 0 {
methodsList = append(methodsList, "CONNECT")
}
if methods&mDELETE != 0 {
methodsList = append(methodsList, "DELETE")
}
if methods&mGET != 0 {
methodsList = append(methodsList, "GET")
}
if methods&mHEAD != 0 {
methodsList = append(methodsList, "HEAD")
}
if methods&mOPTIONS != 0 {
methodsList = append(methodsList, "OPTIONS")
}
if methods&mPATCH != 0 {
methodsList = append(methodsList, "PATCH")
}
if methods&mPOST != 0 {
methodsList = append(methodsList, "POST")
}
if methods&mPUT != 0 {
methodsList = append(methodsList, "PUT")
}
if methods&mTRACE != 0 {
methodsList = append(methodsList, "TRACE")
}
if c.Env == nil {
c.Env = map[string]interface{}{
validMethods: methodsList,
}
} else {
c.Env[validMethods] = methodsList
}
rt.notFound.ServeHTTPC(c, w, r) rt.notFound.ServeHTTPC(c, w, r)
} }
@ -270,6 +321,10 @@ func (m *router) Sub(pattern string, handler interface{}) {
// Set the fallback (i.e., 404) handler for this mux. See the documentation for // Set the fallback (i.e., 404) handler for this mux. See the documentation for
// type Mux for a description of what types are accepted for handler. // type Mux for a description of what types are accepted for handler.
//
// As a convenience, the environment variable "goji.web.validMethods" will be
// set to the list of HTTP methods that could have been routed had they been
// provided on an otherwise identical request
func (m *router) NotFound(handler interface{}) { func (m *router) NotFound(handler interface{}) {
m.notFound = parseHandler(handler) m.notFound = parseHandler(handler)
} }

+ 51
- 1
web/router_test.go View File

@ -3,6 +3,7 @@ package web
import ( import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect"
"regexp" "regexp"
"testing" "testing"
"time" "time"
@ -63,10 +64,12 @@ func (t testPattern) Prefix() string {
return "" return ""
} }
func (t testPattern) Match(r *http.Request, c *C) bool {
func (t testPattern) Match(r *http.Request, c *C, dryrun bool) bool {
return true return true
} }
var _ Pattern = testPattern{}
func TestPatternTypes(t *testing.T) { func TestPatternTypes(t *testing.T) {
t.Parallel() t.Parallel()
rt := makeRouter() rt := makeRouter()
@ -173,3 +176,50 @@ func TestSub(t *testing.T) {
t.Errorf("Timeout waiting for hello") t.Errorf("Timeout waiting for hello")
} }
} }
var validMethodsTable = map[string][]string{
"/hello/carl": {"DELETE", "GET", "PATCH", "POST", "PUT"},
"/hello/bob": {"DELETE", "GET", "HEAD", "PATCH", "PUT"},
"/hola/carl": {"DELETE", "GET", "PUT"},
"/hola/bob": {"DELETE"},
"/does/not/compute": {},
}
func TestValidMethods(t *testing.T) {
t.Parallel()
rt := makeRouter()
ch := make(chan []string, 1)
rt.NotFound(func(c C, w http.ResponseWriter, r *http.Request) {
if c.Env == nil {
ch <- []string{}
return
}
methods, ok := c.Env[validMethods]
if !ok {
ch <- []string{}
return
}
ch <- methods.([]string)
})
rt.Get("/hello/carl", http.NotFound)
rt.Post("/hello/carl", http.NotFound)
rt.Head("/hello/bob", http.NotFound)
rt.Get("/hello/:name", http.NotFound)
rt.Put("/hello/:name", http.NotFound)
rt.Patch("/hello/:name", http.NotFound)
rt.Get("/:greet/carl", http.NotFound)
rt.Put("/:greet/carl", http.NotFound)
rt.Delete("/:greet/:anyone", http.NotFound)
for path, eMethods := range validMethodsTable {
r, _ := http.NewRequest("BOGUS", path, nil)
rt.route(C{}, httptest.NewRecorder(), r)
aMethods := <-ch
if !reflect.DeepEqual(eMethods, aMethods) {
t.Errorf("For %q, expected %v, got %v", path, eMethods,
aMethods)
}
}
}

Loading…
Cancel
Save