Repository: labstack/echo
Pull request #2237 (2022-08-06 20:37:23)

Allow arbitrary HTTP method types to be added as routes
Allow arbitrary HTTP method types to be added as routes. 

This does not affect routing "usual" use-case performance as these arbitrary HTTP method types and underlying map is accessed only when request is of that type. This code is taken from `v5`.

Relates to;
* https://github.com/labstack/echo/issues/1952
* https://github.com/labstack/echo/pull/2173
* https://github.com/labstack/echo/issues/1610
* https://github.com/labstack/echo/issues/1459


Example:
```go
import (
	"fmt"
	"github.com/labstack/echo/v4"
	"log"
	"net/http"
)

func main() {
	e := echo.New()

	e.Add("COPY", "/*", func(c echo.Context) error {
		return c.String(http.StatusOK, "OK COPY")
	})

	if err := e.Start(":8080"); err != http.ErrServerClosed {
		log.Print(fmt.Errorf("error when starting HTTP server: %w", err))
	}
}
```

Output:
```bash
x@x:~/code/$ curl -v -X COPY "http://localhost:8080/something"
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> COPY /something HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=UTF-8
< Date: Sat, 06 Aug 2022 20:34:24 GMT
< Content-Length: 7
< 
* Connection #0 to host localhost left intact
OK COPY
```

codecov[bot]: # [Codecov](https://codecov.io/gh/labstack/echo/pull/2237?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) Report
> Merging [#2237](https://codecov.io/gh/labstack/echo/pull/2237?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) (8f160b2) into [master](https://codecov.io/gh/labstack/echo/commit/a9879ffa6b6fe73e43a0e062884f83eb959e6c1a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) (a9879ff) will **increase** coverage by `0.12%`.
> The diff coverage is `76.92%`.

```diff
@@            Coverage Diff             @@
##           master    #2237      +/-   ##
==========================================
+ Coverage   92.34%   92.46%   +0.12%     
==========================================
  Files          37       37              
  Lines        3123     3134      +11     
==========================================
+ Hits         2884     2898      +14     
+ Misses        150      145       -5     
- Partials       89       91       +2     
```


| [Impacted Files](https://codecov.io/gh/labstack/echo/pull/2237?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) | Coverage Δ | |
|---|---|---|
| [echo.go](https://codecov.io/gh/labstack/echo/pull/2237/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack#diff-ZWNoby5nbw==) | `95.19% <ø> (ø)` | |
| [router.go](https://codecov.io/gh/labstack/echo/pull/2237/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack#diff-cm91dGVyLmdv) | `97.63% <76.92%> (+0.88%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack)

aldas: @lammel could you review this PR?

Patch:
diff --git a/.github/workflows/echo.yml b/.github/workflows/echo.yml
index 69535f0..db60f7f 100644
--- a/.github/workflows/echo.yml
+++ b/.github/workflows/echo.yml
@@ -28,7 +28,7 @@ jobs:
         os: [ubuntu-latest, macos-latest, windows-latest]
         # Each major Go release is supported until there are two newer major releases. https://golang.org/doc/devel/release.html#policy
         # Echo tests with last four major releases
-        go: [1.16, 1.17, 1.18]
+        go: [1.16, 1.17, 1.18, 1.19]
     name: ${{ matrix.os }} @ Go ${{ matrix.go }}
     runs-on: ${{ matrix.os }}
     steps:
@@ -51,8 +51,8 @@ jobs:
           go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
 
       - name: Upload coverage to Codecov
-        if: success() && matrix.go == 1.18 && matrix.os == 'ubuntu-latest'
-        uses: codecov/codecov-action@v1
+        if: success() && matrix.go == 1.19 && matrix.os == 'ubuntu-latest'
+        uses: codecov/codecov-action@v3
         with:
           token:
           fail_ci_if_error: false
@@ -61,7 +61,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest]
-        go: [1.18]
+        go: [1.19]
     name: Benchmark comparison ${{ matrix.os }} @ Go ${{ matrix.go }}
     runs-on: ${{ matrix.os }}
     steps:
diff --git a/README.md b/README.md
index 8b2321f..17e6ed9 100644
--- a/README.md
+++ b/README.md
@@ -93,15 +93,16 @@ func hello(c echo.Context) error {
 
 # Third-party middlewares
 
-| Repository | Description                                                                                                                                                                                                                                                                                                                                                                                                    |
-|------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| [github.com/labstack/echo-contrib](https://github.com/labstack/echo-contrib) | (by Echo team) [casbin](https://github.com/casbin/casbin), [gorilla/sessions](https://github.com/gorilla/sessions), [jaegertracing](github.com/uber/jaeger-client-go), [prometheus](https://github.com/prometheus/client_golang/), [pprof](https://pkg.go.dev/net/http/pprof), [zipkin](https://github.com/openzipkin/zipkin-go) middlewares | 
-| [deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen) | Automatically generate RESTful API documentation with [OpenAPI](https://swagger.io/specification/) Client and Server Code Generator                                                                                                                                                                                                                       |
-| [github.com/swaggo/echo-swagger](https://github.com/swaggo/echo-swagger) | Automatically generate RESTful API documentation with [Swagger](https://swagger.io/) 2.0.                                                                                                                                                                                                                                                        |
-| [github.com/ziflex/lecho](https://github.com/ziflex/lecho) | [Zerolog](https://github.com/rs/zerolog) logging library wrapper for Echo logger interface.                                                                                                                                                                                                                                                                    |
-| [github.com/brpaz/echozap](https://github.com/brpaz/echozap) | Uber´s [Zap](https://github.com/uber-go/zap) logging library wrapper for Echo logger interface.                                                                                                                                                                                                                                                              |
-| [github.com/darkweak/souin/plugins/echo](https://github.com/darkweak/souin/tree/master/plugins/echo) | HTTP cache system based on [Souin](https://github.com/darkweak/souin) to automatically get your endpoints cached. It supports some distributed and non-distributed storage systems depending your needs.                                                                                                             |
-| [github.com/mikestefanello/pagoda](https://github.com/mikestefanello/pagoda) | Rapid, easy full-stack web development starter kit built with Echo.
+| Repository                                                                                           | Description                                                                                                                                                                                                                                                                                                                                  |
+|------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [github.com/labstack/echo-contrib](https://github.com/labstack/echo-contrib)                         | (by Echo team) [casbin](https://github.com/casbin/casbin), [gorilla/sessions](https://github.com/gorilla/sessions), [jaegertracing](github.com/uber/jaeger-client-go), [prometheus](https://github.com/prometheus/client_golang/), [pprof](https://pkg.go.dev/net/http/pprof), [zipkin](https://github.com/openzipkin/zipkin-go) middlewares | 
+| [deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen)                                      | Automatically generate RESTful API documentation with [OpenAPI](https://swagger.io/specification/) Client and Server Code Generator                                                                                                                                                                                                          |
+| [github.com/swaggo/echo-swagger](https://github.com/swaggo/echo-swagger)                             | Automatically generate RESTful API documentation with [Swagger](https://swagger.io/) 2.0.                                                                                                                                                                                                                                                    |
+| [github.com/ziflex/lecho](https://github.com/ziflex/lecho)                                           | [Zerolog](https://github.com/rs/zerolog) logging library wrapper for Echo logger interface.                                                                                                                                                                                                                                                  |
+| [github.com/brpaz/echozap](https://github.com/brpaz/echozap)                                         | Uber´s [Zap](https://github.com/uber-go/zap) logging library wrapper for Echo logger interface.                                                                                                                                                                                                                                              |
+| [github.com/darkweak/souin/plugins/echo](https://github.com/darkweak/souin/tree/master/plugins/echo) | HTTP cache system based on [Souin](https://github.com/darkweak/souin) to automatically get your endpoints cached. It supports some distributed and non-distributed storage systems depending your needs.                                                                                                                                     |
+| [github.com/mikestefanello/pagoda](https://github.com/mikestefanello/pagoda)                         | Rapid, easy full-stack web development starter kit built with Echo.                                                                                                                                                                                                                                                                          |
+| [github.com/go-woo/protoc-gen-echo](https://github.com/go-woo/protoc-gen-echo)                       | ProtoBuf generate Echo server side code                                                                                                                                                                                                                                                                                                      |
 
 Please send a PR to add your own library here.
 
diff --git a/echo.go b/echo.go
index 5b10d58..5738578 100644
--- a/echo.go
+++ b/echo.go
@@ -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 {
diff --git a/router.go b/router.go
index 90102a2..23c5bd3 100644
--- a/router.go
+++ b/router.go
@@ -51,6 +51,7 @@ type (
 		put         *routeMethod
 		trace       *routeMethod
 		report      *routeMethod
+		anyOther    map[string]*routeMethod
 		allowHeader string
 	}
 )
@@ -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
 }
 
@@ -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()
 }
 
@@ -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()
@@ -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]
 	}
 }
 


Test patch:
diff --git a/router_test.go b/router_test.go
index 1b0c409..a954210 100644
--- a/router_test.go
+++ b/router_test.go
@@ -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
@@ -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