Skip to content

Commit

Permalink
Allow arbitrary HTTP method types to be added as routes
Browse files Browse the repository at this point in the history
  • Loading branch information
aldas committed Aug 10, 2022
1 parent a327884 commit cba12a5
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 3 deletions.
5 changes: 4 additions & 1 deletion echo.go
Expand Up @@ -492,8 +492,11 @@ func (e *Echo) RouteNotFound(path string, h HandlerFunc, m ...MiddlewareFunc) *R
return e.Add(RouteNotFound, path, h, m...)
}

// Any registers a new route for all HTTP methods and path with matching handler
// Any registers a new route for all HTTP methods (supported by Echo) and path with matching handler
// in the router with optional route-level middleware.
//
// Note: this method only adds specific set of supported HTTP methods as handler and is not true
// "catch-any-arbitrary-method" way of matching requests.
func (e *Echo) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
routes := make([]*Route, len(methods))
for i, m := range methods {
Expand Down
19 changes: 17 additions & 2 deletions router.go
Expand Up @@ -51,6 +51,7 @@ type (
put *routeMethod
trace *routeMethod
report *routeMethod
anyOther map[string]*routeMethod
allowHeader string
}
)
Expand All @@ -75,7 +76,8 @@ func (m *routeMethods) isHandler() bool {
m.propfind != nil ||
m.put != nil ||
m.trace != nil ||
m.report != nil
m.report != nil ||
len(m.anyOther) != 0
// RouteNotFound/404 is not considered as a handler
}

Expand Down Expand Up @@ -121,6 +123,10 @@ func (m *routeMethods) updateAllowHeader() {
if m.report != nil {
buf.WriteString(", REPORT")
}
for method := range m.anyOther { // for simplicity, we use map and therefore order is not deterministic here
buf.WriteString(", ")
buf.WriteString(method)
}
m.allowHeader = buf.String()
}

Expand Down Expand Up @@ -408,6 +414,15 @@ func (n *node) addMethod(method string, h *routeMethod) {
case RouteNotFound:
n.notFoundHandler = h
return // RouteNotFound/404 is not considered as a handler so no further logic needs to be executed
default:
if n.methods.anyOther == nil {
n.methods.anyOther = make(map[string]*routeMethod)
}
if h.handler == nil {
delete(n.methods.anyOther, method)
} else {
n.methods.anyOther[method] = h
}
}

n.methods.updateAllowHeader()
Expand Down Expand Up @@ -439,7 +454,7 @@ func (n *node) findMethod(method string) *routeMethod {
case REPORT:
return n.methods.report
default: // RouteNotFound/404 is not considered as a handler
return nil
return n.methods.anyOther[method]
}
}

Expand Down
80 changes: 80 additions & 0 deletions router_test.go
Expand Up @@ -716,6 +716,67 @@ func TestRouterParam(t *testing.T) {
}
}

func TestRouter_addAndMatchAllSupportedMethods(t *testing.T) {
var testCases = []struct {
name string
givenNoAddRoute bool
whenMethod string
expectPath string
expectError string
}{
{name: "ok, CONNECT", whenMethod: http.MethodConnect},
{name: "ok, DELETE", whenMethod: http.MethodDelete},
{name: "ok, GET", whenMethod: http.MethodGet},
{name: "ok, HEAD", whenMethod: http.MethodHead},
{name: "ok, OPTIONS", whenMethod: http.MethodOptions},
{name: "ok, PATCH", whenMethod: http.MethodPatch},
{name: "ok, POST", whenMethod: http.MethodPost},
{name: "ok, PROPFIND", whenMethod: PROPFIND},
{name: "ok, PUT", whenMethod: http.MethodPut},
{name: "ok, TRACE", whenMethod: http.MethodTrace},
{name: "ok, REPORT", whenMethod: REPORT},
{name: "ok, NON_TRADITIONAL_METHOD", whenMethod: "NON_TRADITIONAL_METHOD"},
{
name: "ok, NOT_EXISTING_METHOD",
whenMethod: "NOT_EXISTING_METHOD",
givenNoAddRoute: true,
expectPath: "/*",
expectError: "code=405, message=Method Not Allowed",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
e := New()

e.GET("/*", handlerFunc)

if !tc.givenNoAddRoute {
e.Add(tc.whenMethod, "/my/*", handlerFunc)
}

req := httptest.NewRequest(tc.whenMethod, "/my/some-url", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec).(*context)

e.router.Find(tc.whenMethod, "/my/some-url", c)
err := c.handler(c)

if tc.expectError != "" {
assert.EqualError(t, err, tc.expectError)
} else {
assert.NoError(t, err)
}

expectPath := "/my/*"
if tc.expectPath != "" {
expectPath = tc.expectPath
}
assert.Equal(t, expectPath, c.Path())
})
}
}

func TestMethodNotAllowedAndNotFound(t *testing.T) {
e := New()
r := e.router
Expand Down Expand Up @@ -2634,6 +2695,25 @@ func TestRouterHandleMethodOptions(t *testing.T) {
}
}

func TestRouterAllowHeaderForAnyOtherMethodType(t *testing.T) {
e := New()
r := e.router

r.Add(http.MethodGet, "/users", handlerFunc)
r.Add("COPY", "/users", handlerFunc)
r.Add("LOCK", "/users", handlerFunc)

req := httptest.NewRequest("TEST", "/users", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec).(*context)

r.Find("TEST", "/users", c)
err := c.handler(c)

assert.EqualError(t, err, "code=405, message=Method Not Allowed")
assert.ElementsMatch(t, []string{"COPY", "GET", "LOCK", "OPTIONS"}, strings.Split(c.Response().Header().Get(HeaderAllow), ", "))
}

func benchmarkRouterRoutes(b *testing.B, routes []*Route, routesToFind []*Route) {
e := New()
r := e.router
Expand Down

0 comments on commit cba12a5

Please sign in to comment.