Skip to content

Commit bcdc802

Browse files
committed
router: sort allowed methods
Fixes #248
1 parent b64ea83 commit bcdc802

File tree

2 files changed

+52
-23
lines changed

2 files changed

+52
-23
lines changed

router.go

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ package httprouter
7979
import (
8080
"context"
8181
"net/http"
82+
"strings"
8283
)
8384

8485
// Handle is a function that can be registered to a route to handle HTTP
@@ -313,18 +314,15 @@ func (r *Router) Lookup(method, path string) (Handle, Params, bool) {
313314
}
314315

315316
func (r *Router) allowed(path, reqMethod string) (allow string) {
317+
allowed := make([]string, 0, 9)
318+
316319
if path == "*" { // server-wide
317320
for method := range r.trees {
318321
if method == http.MethodOptions {
319322
continue
320323
}
321-
322-
// add request method to list of allowed methods
323-
if len(allow) == 0 {
324-
allow = method
325-
} else {
326-
allow += ", " + method
327-
}
324+
// Add request method to list of allowed methods
325+
allowed = append(allowed, method)
328326
}
329327
} else { // specific path
330328
for method := range r.trees {
@@ -335,17 +333,27 @@ func (r *Router) allowed(path, reqMethod string) (allow string) {
335333

336334
handle, _, _ := r.trees[method].getValue(path)
337335
if handle != nil {
338-
// add request method to list of allowed methods
339-
if len(allow) == 0 {
340-
allow = method
341-
} else {
342-
allow += ", " + method
343-
}
336+
// Add request method to list of allowed methods
337+
allowed = append(allowed, method)
344338
}
345339
}
346340
}
347-
if len(allow) > 0 {
348-
allow += ", OPTIONS"
341+
342+
if len(allowed) > 0 {
343+
// Add request method to list of allowed methods
344+
allowed = append(allowed, http.MethodOptions)
345+
346+
// Sort allowed methods.
347+
// sort.Strings(allowed) unfortunately causes unnecessary allocations
348+
// due to allowed being moved to the heap and interface conversion
349+
for i, l := 1, len(allowed); i < l; i++ {
350+
for j := i; j > 0 && allowed[j] < allowed[j-1]; j-- {
351+
allowed[j], allowed[j-1] = allowed[j-1], allowed[j]
352+
}
353+
}
354+
355+
// return as comma separated list
356+
return strings.Join(allowed, ", ")
349357
}
350358
return
351359
}

router_test.go

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,27 @@ func TestRouterChaining(t *testing.T) {
216216
}
217217
}
218218

219+
func BenchmarkAllowed(b *testing.B) {
220+
handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {}
221+
222+
router := New()
223+
router.POST("/path", handlerFunc)
224+
router.GET("/path", handlerFunc)
225+
226+
b.Run("Global", func(b *testing.B) {
227+
b.ReportAllocs()
228+
for i := 0; i < b.N; i++ {
229+
_ = router.allowed("*", http.MethodOptions)
230+
}
231+
})
232+
b.Run("Path", func(b *testing.B) {
233+
b.ReportAllocs()
234+
for i := 0; i < b.N; i++ {
235+
_ = router.allowed("/path", http.MethodOptions)
236+
}
237+
})
238+
}
239+
219240
func TestRouterOPTIONS(t *testing.T) {
220241
handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {}
221242

@@ -229,7 +250,7 @@ func TestRouterOPTIONS(t *testing.T) {
229250
router.ServeHTTP(w, r)
230251
if !(w.Code == http.StatusOK) {
231252
t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header())
232-
} else if allow := w.Header().Get("Allow"); allow != "POST, OPTIONS" {
253+
} else if allow := w.Header().Get("Allow"); allow != "OPTIONS, POST" {
233254
t.Error("unexpected Allow header value: " + allow)
234255
}
235256

@@ -239,7 +260,7 @@ func TestRouterOPTIONS(t *testing.T) {
239260
router.ServeHTTP(w, r)
240261
if !(w.Code == http.StatusOK) {
241262
t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header())
242-
} else if allow := w.Header().Get("Allow"); allow != "POST, OPTIONS" {
263+
} else if allow := w.Header().Get("Allow"); allow != "OPTIONS, POST" {
243264
t.Error("unexpected Allow header value: " + allow)
244265
}
245266

@@ -260,7 +281,7 @@ func TestRouterOPTIONS(t *testing.T) {
260281
router.ServeHTTP(w, r)
261282
if !(w.Code == http.StatusOK) {
262283
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" {
284+
} else if allow := w.Header().Get("Allow"); allow != "GET, OPTIONS, POST" {
264285
t.Error("unexpected Allow header value: " + allow)
265286
}
266287

@@ -270,7 +291,7 @@ func TestRouterOPTIONS(t *testing.T) {
270291
router.ServeHTTP(w, r)
271292
if !(w.Code == http.StatusOK) {
272293
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" {
294+
} else if allow := w.Header().Get("Allow"); allow != "GET, OPTIONS, POST" {
274295
t.Error("unexpected Allow header value: " + allow)
275296
}
276297

@@ -287,7 +308,7 @@ func TestRouterOPTIONS(t *testing.T) {
287308
router.ServeHTTP(w, r)
288309
if !(w.Code == http.StatusOK) {
289310
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" {
311+
} else if allow := w.Header().Get("Allow"); allow != "GET, OPTIONS, POST" {
291312
t.Error("unexpected Allow header value: " + allow)
292313
}
293314
if custom {
@@ -318,7 +339,7 @@ func TestRouterNotAllowed(t *testing.T) {
318339
router.ServeHTTP(w, r)
319340
if !(w.Code == http.StatusMethodNotAllowed) {
320341
t.Errorf("NotAllowed handling failed: Code=%d, Header=%v", w.Code, w.Header())
321-
} else if allow := w.Header().Get("Allow"); allow != "POST, OPTIONS" {
342+
} else if allow := w.Header().Get("Allow"); allow != "OPTIONS, POST" {
322343
t.Error("unexpected Allow header value: " + allow)
323344
}
324345

@@ -332,7 +353,7 @@ func TestRouterNotAllowed(t *testing.T) {
332353
router.ServeHTTP(w, r)
333354
if !(w.Code == http.StatusMethodNotAllowed) {
334355
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" {
356+
} else if allow := w.Header().Get("Allow"); allow != "DELETE, OPTIONS, POST" {
336357
t.Error("unexpected Allow header value: " + allow)
337358
}
338359

@@ -350,7 +371,7 @@ func TestRouterNotAllowed(t *testing.T) {
350371
if w.Code != http.StatusTeapot {
351372
t.Errorf("unexpected response code %d want %d", w.Code, http.StatusTeapot)
352373
}
353-
if allow := w.Header().Get("Allow"); allow != "POST, DELETE, OPTIONS" && allow != "DELETE, POST, OPTIONS" {
374+
if allow := w.Header().Get("Allow"); allow != "DELETE, OPTIONS, POST" {
354375
t.Error("unexpected Allow header value: " + allow)
355376
}
356377
}

0 commit comments

Comments
 (0)