Skip to content

Commit 8d90774

Browse files
author
David Budworth
authored
Merge branch 'master' into routegroup
2 parents bb7dc83 + 8a45e95 commit 8d90774

File tree

6 files changed

+454
-196
lines changed

6 files changed

+454
-196
lines changed

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@ go:
55
- 1.2
66
- 1.3
77
- 1.4
8+
- 1.5
9+
- 1.6
10+
- 1.7
811
- tip

README.md

Lines changed: 92 additions & 139 deletions
Large diffs are not rendered by default.

router.go

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,19 @@ type Router struct {
138138
// handler.
139139
HandleMethodNotAllowed bool
140140

141+
// If enabled, the router automatically replies to OPTIONS requests.
142+
// Custom OPTIONS handlers take priority over automatic replies.
143+
HandleOPTIONS bool
144+
141145
// Configurable http.Handler which is called when no matching route is
142146
// found. If it is not set, http.NotFound is used.
143147
NotFound http.Handler
144148

145149
// Configurable http.Handler which is called when a request
146150
// cannot be routed and HandleMethodNotAllowed is true.
147151
// If it is not set, http.Error with http.StatusMethodNotAllowed is used.
152+
// The "Allow" header with allowed request methods is set before the handler
153+
// is called.
148154
MethodNotAllowed http.Handler
149155

150156
// Function to handle panics recovered from http handlers.
@@ -165,6 +171,7 @@ func New() *Router {
165171
RedirectTrailingSlash: true,
166172
RedirectFixedPath: true,
167173
HandleMethodNotAllowed: true,
174+
HandleOPTIONS: true,
168175
}
169176
}
170177

@@ -290,15 +297,53 @@ func (r *Router) Lookup(method, path string) (Handle, Params, bool) {
290297
return nil, nil, false
291298
}
292299

300+
func (r *Router) allowed(path, reqMethod string) (allow string) {
301+
if path == "*" { // server-wide
302+
for method := range r.trees {
303+
if method == "OPTIONS" {
304+
continue
305+
}
306+
307+
// add request method to list of allowed methods
308+
if len(allow) == 0 {
309+
allow = method
310+
} else {
311+
allow += ", " + method
312+
}
313+
}
314+
} else { // specific path
315+
for method := range r.trees {
316+
// Skip the requested method - we already tried this one
317+
if method == reqMethod || method == "OPTIONS" {
318+
continue
319+
}
320+
321+
handle, _, _ := r.trees[method].getValue(path)
322+
if handle != nil {
323+
// add request method to list of allowed methods
324+
if len(allow) == 0 {
325+
allow = method
326+
} else {
327+
allow += ", " + method
328+
}
329+
}
330+
}
331+
}
332+
if len(allow) > 0 {
333+
allow += ", OPTIONS"
334+
}
335+
return
336+
}
337+
293338
// ServeHTTP makes the router implement the http.Handler interface.
294339
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
295340
if r.PanicHandler != nil {
296341
defer r.recv(w, req)
297342
}
298343

299-
if root := r.trees[req.Method]; root != nil {
300-
path := req.URL.Path
344+
path := req.URL.Path
301345

346+
if root := r.trees[req.Method]; root != nil {
302347
if handle, ps, tsr := root.getValue(path); handle != nil {
303348
handle(w, req, ps)
304349
return
@@ -335,16 +380,19 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
335380
}
336381
}
337382

338-
// Handle 405
339-
if r.HandleMethodNotAllowed {
340-
for method := range r.trees {
341-
// Skip the requested method - we already tried this one
342-
if method == req.Method {
343-
continue
383+
if req.Method == "OPTIONS" {
384+
// Handle OPTIONS requests
385+
if r.HandleOPTIONS {
386+
if allow := r.allowed(path, req.Method); len(allow) > 0 {
387+
w.Header().Set("Allow", allow)
388+
return
344389
}
345-
346-
handle, _, _ := r.trees[method].getValue(req.URL.Path)
347-
if handle != nil {
390+
}
391+
} else {
392+
// Handle 405
393+
if r.HandleMethodNotAllowed {
394+
if allow := r.allowed(path, req.Method); len(allow) > 0 {
395+
w.Header().Set("Allow", allow)
348396
if r.MethodNotAllowed != nil {
349397
r.MethodNotAllowed.ServeHTTP(w, req)
350398
} else {

router_test.go

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,11 @@ func TestRouter(t *testing.T) {
6868
}
6969

7070
type handlerStruct struct {
71-
handeled *bool
71+
handled *bool
7272
}
7373

7474
func (h handlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
75-
*h.handeled = true
75+
*h.handled = true
7676
}
7777

7878
func TestRouterAPI(t *testing.T) {
@@ -216,20 +216,127 @@ func TestRouterChaining(t *testing.T) {
216216
}
217217
}
218218

219+
func TestRouterOPTIONS(t *testing.T) {
220+
handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {}
221+
222+
router := New()
223+
router.POST("/path", handlerFunc)
224+
225+
// test not allowed
226+
// * (server)
227+
r, _ := http.NewRequest("OPTIONS", "*", nil)
228+
w := httptest.NewRecorder()
229+
router.ServeHTTP(w, r)
230+
if !(w.Code == http.StatusOK) {
231+
t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header())
232+
} else if allow := w.Header().Get("Allow"); allow != "POST, OPTIONS" {
233+
t.Error("unexpected Allow header value: " + allow)
234+
}
235+
236+
// path
237+
r, _ = http.NewRequest("OPTIONS", "/path", nil)
238+
w = httptest.NewRecorder()
239+
router.ServeHTTP(w, r)
240+
if !(w.Code == http.StatusOK) {
241+
t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header())
242+
} else if allow := w.Header().Get("Allow"); allow != "POST, OPTIONS" {
243+
t.Error("unexpected Allow header value: " + allow)
244+
}
245+
246+
r, _ = http.NewRequest("OPTIONS", "/doesnotexist", nil)
247+
w = httptest.NewRecorder()
248+
router.ServeHTTP(w, r)
249+
if !(w.Code == http.StatusNotFound) {
250+
t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header())
251+
}
252+
253+
// add another method
254+
router.GET("/path", handlerFunc)
255+
256+
// test again
257+
// * (server)
258+
r, _ = http.NewRequest("OPTIONS", "*", nil)
259+
w = httptest.NewRecorder()
260+
router.ServeHTTP(w, r)
261+
if !(w.Code == http.StatusOK) {
262+
t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header())
263+
} else if allow := w.Header().Get("Allow"); allow != "POST, GET, OPTIONS" && allow != "GET, POST, OPTIONS" {
264+
t.Error("unexpected Allow header value: " + allow)
265+
}
266+
267+
// path
268+
r, _ = http.NewRequest("OPTIONS", "/path", nil)
269+
w = httptest.NewRecorder()
270+
router.ServeHTTP(w, r)
271+
if !(w.Code == http.StatusOK) {
272+
t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header())
273+
} else if allow := w.Header().Get("Allow"); allow != "POST, GET, OPTIONS" && allow != "GET, POST, OPTIONS" {
274+
t.Error("unexpected Allow header value: " + allow)
275+
}
276+
277+
// custom handler
278+
var custom bool
279+
router.OPTIONS("/path", func(w http.ResponseWriter, r *http.Request, _ Params) {
280+
custom = true
281+
})
282+
283+
// test again
284+
// * (server)
285+
r, _ = http.NewRequest("OPTIONS", "*", nil)
286+
w = httptest.NewRecorder()
287+
router.ServeHTTP(w, r)
288+
if !(w.Code == http.StatusOK) {
289+
t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header())
290+
} else if allow := w.Header().Get("Allow"); allow != "POST, GET, OPTIONS" && allow != "GET, POST, OPTIONS" {
291+
t.Error("unexpected Allow header value: " + allow)
292+
}
293+
if custom {
294+
t.Error("custom handler called on *")
295+
}
296+
297+
// path
298+
r, _ = http.NewRequest("OPTIONS", "/path", nil)
299+
w = httptest.NewRecorder()
300+
router.ServeHTTP(w, r)
301+
if !(w.Code == http.StatusOK) {
302+
t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header())
303+
}
304+
if !custom {
305+
t.Error("custom handler not called")
306+
}
307+
}
308+
219309
func TestRouterNotAllowed(t *testing.T) {
220310
handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {}
221311

222312
router := New()
223313
router.POST("/path", handlerFunc)
224314

225-
// Test not allowed
315+
// test not allowed
226316
r, _ := http.NewRequest("GET", "/path", nil)
227317
w := httptest.NewRecorder()
228318
router.ServeHTTP(w, r)
229319
if !(w.Code == http.StatusMethodNotAllowed) {
230320
t.Errorf("NotAllowed handling failed: Code=%d, Header=%v", w.Code, w.Header())
321+
} else if allow := w.Header().Get("Allow"); allow != "POST, OPTIONS" {
322+
t.Error("unexpected Allow header value: " + allow)
323+
}
324+
325+
// add another method
326+
router.DELETE("/path", handlerFunc)
327+
router.OPTIONS("/path", handlerFunc) // must be ignored
328+
329+
// test again
330+
r, _ = http.NewRequest("GET", "/path", nil)
331+
w = httptest.NewRecorder()
332+
router.ServeHTTP(w, r)
333+
if !(w.Code == http.StatusMethodNotAllowed) {
334+
t.Errorf("NotAllowed handling failed: Code=%d, Header=%v", w.Code, w.Header())
335+
} else if allow := w.Header().Get("Allow"); allow != "POST, DELETE, OPTIONS" && allow != "DELETE, POST, OPTIONS" {
336+
t.Error("unexpected Allow header value: " + allow)
231337
}
232338

339+
// test custom handler
233340
w = httptest.NewRecorder()
234341
responseText := "custom method"
235342
router.MethodNotAllowed = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -243,6 +350,9 @@ func TestRouterNotAllowed(t *testing.T) {
243350
if w.Code != http.StatusTeapot {
244351
t.Errorf("unexpected response code %d want %d", w.Code, http.StatusTeapot)
245352
}
353+
if allow := w.Header().Get("Allow"); allow != "POST, DELETE, OPTIONS" && allow != "DELETE, POST, OPTIONS" {
354+
t.Error("unexpected Allow header value: " + allow)
355+
}
246356
}
247357

248358
func TestRouterNotFound(t *testing.T) {

0 commit comments

Comments
 (0)