Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow arbitrary HTTP method types to be added as routes #2237

Merged
merged 1 commit into from Aug 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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