Repository: labstack/echo
Pull request #1764 (2021-02-04 20:31:44)

Add CHANGELOG.md for historic tracking of changes
With this PR we will start to track a human-readable CHANGELOG for every official release of Echo.

This will ensure the change information will be available even if github release wont exist anymore.
It shall also be a spot for people to check for recent changes.

lammel: Suggested format for the changelog by @aldas is https://keepachangelog.com/en/1.0.0/
codecov[bot]: # [Codecov](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=h1) Report
> Merging [#1764](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=desc) (68914a6) into [master](https://codecov.io/gh/labstack/echo/commit/7c8592a7e05370b21cc02cc2edaee6b2355fd83a?el=desc) (7c8592a) will **decrease** coverage by `0.57%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/labstack/echo/pull/1764/graphs/tree.svg?width=650&height=150&src=pr&token=vqnT76l79i)](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1764      +/-   ##
==========================================
- Coverage   89.72%   89.14%   -0.58%     
==========================================
  Files          32       32              
  Lines        2667     2672       +5     
==========================================
- Hits         2393     2382      -11     
- Misses        175      187      +12     
- Partials       99      103       +4     
```


| [Impacted Files](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [binder.go](https://codecov.io/gh/labstack/echo/pull/1764/diff?src=pr&el=tree#diff-YmluZGVyLmdv) | `96.83% <0.00%> (-3.17%)` | :arrow_down: |
| [middleware/proxy.go](https://codecov.io/gh/labstack/echo/pull/1764/diff?src=pr&el=tree#diff-bWlkZGxld2FyZS9wcm94eS5nbw==) | `62.35% <0.00%> (+1.37%)` | :arrow_up: |
| [middleware/rewrite.go](https://codecov.io/gh/labstack/echo/pull/1764/diff?src=pr&el=tree#diff-bWlkZGxld2FyZS9yZXdyaXRlLmdv) | `76.47% <0.00%> (+3.13%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=footer). Last update [7c8592a...68914a6](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

codecov[bot]: # [Codecov](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=h1) Report
> Merging [#1764](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=desc) (9d91448) into [master](https://codecov.io/gh/labstack/echo/commit/932976ded6e22eb0d80f73c540aef0849b2047af?el=desc) (932976d) will **decrease** coverage by `0.59%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/labstack/echo/pull/1764/graphs/tree.svg?width=650&height=150&src=pr&token=vqnT76l79i)](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1764      +/-   ##
==========================================
- Coverage   89.74%   89.14%   -0.60%     
==========================================
  Files          32       32              
  Lines        2672     2672              
==========================================
- Hits         2398     2382      -16     
- Misses        175      187      +12     
- Partials       99      103       +4     
```


| [Impacted Files](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [binder.go](https://codecov.io/gh/labstack/echo/pull/1764/diff?src=pr&el=tree#diff-YmluZGVyLmdv) | `96.83% <0.00%> (-3.17%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=footer). Last update [932976d...313ff75](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

codecov[bot]: # [Codecov](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=h1) Report
> Merging [#1764](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=desc) (313ff75) into [master](https://codecov.io/gh/labstack/echo/commit/932976ded6e22eb0d80f73c540aef0849b2047af?el=desc) (932976d) will **decrease** coverage by `0.59%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/labstack/echo/pull/1764/graphs/tree.svg?width=650&height=150&src=pr&token=vqnT76l79i)](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1764      +/-   ##
==========================================
- Coverage   89.74%   89.14%   -0.60%     
==========================================
  Files          32       32              
  Lines        2672     2672              
==========================================
- Hits         2398     2382      -16     
- Misses        175      187      +12     
- Partials       99      103       +4     
```


| [Impacted Files](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [binder.go](https://codecov.io/gh/labstack/echo/pull/1764/diff?src=pr&el=tree#diff-YmluZGVyLmdv) | `96.83% <0.00%> (-3.17%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=footer). Last update [932976d...313ff75](https://codecov.io/gh/labstack/echo/pull/1764?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..33f5587
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,73 @@
+# Changelog
+
+## v4.2.0 - 2020-02-11
+
+**Important notes**
+
+The behaviour for binding data has been reworked for compatibility with echo before v4.1.11 by
+enforcing `explicit tagging` for processing parameters. This **may break** your code if you 
+expect combined handling of query/path/form params.
+Please see the updated documentation for [request](https://echo.labstack.com/guide/request) and [binding](https://echo.labstack.com/guide/request)
+
+The handling for rewrite rules has been slightly adjusted to expand `*` to a non-greedy `(.*?)` capture group. This is only relevant if multiple asterisks are used in your rules.
+Please see [rewrite](https://echo.labstack.com/middleware/rewrite) and [proxy](https://echo.labstack.com/middleware/proxy) for details.
+
+**Security**
+
+* Fix directory traversal vulnerability for Windows (#1718, little-cui)
+* Fix open redirect vulnerability with trailing slash (#1771,#1775 aldas,GeoffreyFrogeye)
+
+**Enhancements**
+
+* Add Echo#ListenerNetwork as configuration (#1667, pafuent)
+* Add ability to change the status code using response beforeFuncs (#1706, RashadAnsari)
+* Echo server startup to allow data race free access to listener address
+* Binder: Restore pre v4.1.11 behaviour for c.Bind() to use query params only for GET or DELETE methods (#1727, aldas)
+* Binder: Add separate methods to bind only query params, path params or request body (#1681, aldas)
+* Binder: New fluent binder for query/path/form parameter binding (#1717, #1736, aldas)
+* Router: Performance improvements for missed routes (#1689, pafuent)
+* Router: Improve performance for Real-IP detection using IndexByte instead of Split (#1640, imxyb)
+* Middleware: Support real regex rules for rewrite and proxy middleware (#1767)
+* Middleware: New rate limiting middleware (#1724, iambenkay)
+* Middleware: New timeout middleware implementation for go1.13+ (#1743, )
+* Middleware: Allow regex pattern for CORS middleware (#1623, KlotzAndrew)
+* Middleware: Add IgnoreBase parameter to static middleware (#1701, lnenad, iambenkay)
+* Middleware: Add an optional custom function to CORS middleware to validate origin (#1651, curvegrid)
+* Middleware: Support form fields in JWT middleware (#1704, rkfg)
+* Middleware: Use sync.Pool for (de)compress middleware to improve performance (#1699, #1672, pafuent)
+* Middleware: Add decompress middleware to support gzip compressed requests (#1687, arun0009)
+* Middleware: Add ErrJWTInvalid for JWT middleware (#1627, juanbelieni)
+* Middleware: Add SameSite mode for CSRF cookies to support iframes (#1524, pr0head)
+
+**Fixes**
+
+* Fix handling of special trailing slash case for partial prefix (#1741, stffabi)
+* Fix handling of static routes with trailing slash (#1747)
+* Fix Static files route not working (#1671, pwli0755, lammel)
+* Fix use of caret(^) in regex for rewrite middleware (#1588, chotow)
+* Fix Echo#Reverse for Any type routes (#1695, pafuent)
+* Fix Router#Find panic with infinite loop (#1661, pafuent)
+* Fix Router#Find panic fails on Param paths (#1659, pafuent)
+* Fix DefaultHTTPErrorHandler with Debug=true (#1477, lammel)
+* Fix incorrect CORS headers (#1669, ulasakdeniz)
+* Fix proxy middleware rewritePath to use url with updated tests (#1630, arun0009)
+* Fix rewritePath for proxy middleware to use escaped path in (#1628, arun0009)
+* Remove unless defer (#1656, imxyb)
+
+**General**
+
+* New maintainers for Echo: Roland Lammel (@lammel) and Pablo Andres Fuente (@pafuent)
+* Add GitHub action to compare benchmarks (#1702, pafuent)
+* Binding query/path params and form fields to struct only works for explicit tags (#1729,#1734, aldas)
+* Add support for Go 1.15 in CI (#1683, asahasrabuddhe)
+* Add test for request id to remain unchanged if provided (#1719, iambenkay)
+* Refactor echo instance listener access and startup to speed up testing (#1735, aldas)
+* Refactor and improve various tests for binding and routing
+* Run test workflow only for relevant changes (#1637, #1636, pofl)
+* Update .travis.yml (#1662, santosh653)
+* Update README.md with an recents framework benchmark (#1679, pafuent)
+
+This release was made possible by **over 100 commits** from more than **20 contributors**:
+asahasrabuddhe, aldas, AndrewKlotz, arun0009, chotow, curvegrid, iambenkay, imxyb, 
+juanbelieni,  lammel, little-cui, lnenad, pafuent, pofl, pr0head, pwli, RashadAnsari, 
+rkfg, santosh653, segfiner, stffabi, ulasakdeniz
diff --git a/middleware/slash.go b/middleware/slash.go
index 0492b33..4188675 100644
--- a/middleware/slash.go
+++ b/middleware/slash.go
@@ -60,7 +60,7 @@ func AddTrailingSlashWithConfig(config TrailingSlashConfig) echo.MiddlewareFunc
 
 				// Redirect
 				if config.RedirectCode != 0 {
-					return c.Redirect(config.RedirectCode, uri)
+					return c.Redirect(config.RedirectCode, sanitizeURI(uri))
 				}
 
 				// Forward
@@ -108,7 +108,7 @@ func RemoveTrailingSlashWithConfig(config TrailingSlashConfig) echo.MiddlewareFu
 
 				// Redirect
 				if config.RedirectCode != 0 {
-					return c.Redirect(config.RedirectCode, uri)
+					return c.Redirect(config.RedirectCode, sanitizeURI(uri))
 				}
 
 				// Forward
@@ -119,3 +119,12 @@ func RemoveTrailingSlashWithConfig(config TrailingSlashConfig) echo.MiddlewareFu
 		}
 	}
 }
+
+func sanitizeURI(uri string) string {
+	// double slash `\\`, `//` or even `\/` is absolute uri for browsers and by redirecting request to that uri
+	// we are vulnerable to open redirect attack. so replace all slashes from the beginning with single slash
+	if len(uri) > 1 && (uri[0] == '\\' || uri[0] == '/') && (uri[1] == '\\' || uri[1] == '/') {
+		uri = "/" + strings.TrimLeft(uri, `/\`)
+	}
+	return uri
+}


Test patch:
diff --git a/middleware/slash_test.go b/middleware/slash_test.go
index 2a8e9ee..ddb0710 100644
--- a/middleware/slash_test.go
+++ b/middleware/slash_test.go
@@ -9,88 +9,270 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+func TestAddTrailingSlashWithConfig(t *testing.T) {
+	var testCases = []struct {
+		whenURL        string
+		whenMethod     string
+		expectPath     string
+		expectLocation []string
+		expectStatus   int
+	}{
+		{
+			whenURL:        "/add-slash",
+			whenMethod:     http.MethodGet,
+			expectPath:     "/add-slash",
+			expectLocation: []string{`/add-slash/`},
+		},
+		{
+			whenURL:        "/add-slash?key=value",
+			whenMethod:     http.MethodGet,
+			expectPath:     "/add-slash",
+			expectLocation: []string{`/add-slash/?key=value`},
+		},
+		{
+			whenURL:        "/",
+			whenMethod:     http.MethodConnect,
+			expectPath:     "/",
+			expectLocation: nil,
+			expectStatus:   http.StatusOK,
+		},
+		// cases for open redirect vulnerability
+		{
+			whenURL:        "http://localhost:1323/%5Cexample.com",
+			expectPath:     `/\example.com`,
+			expectLocation: []string{`/example.com/`},
+		},
+		{
+			whenURL:        `http://localhost:1323/\example.com`,
+			expectPath:     `/\example.com`,
+			expectLocation: []string{`/example.com/`},
+		},
+		{
+			whenURL:        `http://localhost:1323/\\%5C////%5C\\\example.com`,
+			expectPath:     `/\\\////\\\\example.com`,
+			expectLocation: []string{`/example.com/`},
+		},
+		{
+			whenURL:        "http://localhost:1323//example.com",
+			expectPath:     `//example.com`,
+			expectLocation: []string{`/example.com/`},
+		},
+		{
+			whenURL:        "http://localhost:1323/%5C%5C",
+			expectPath:     `/\\`,
+			expectLocation: []string{`/`},
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.whenURL, func(t *testing.T) {
+			e := echo.New()
+
+			mw := AddTrailingSlashWithConfig(TrailingSlashConfig{
+				RedirectCode: http.StatusMovedPermanently,
+			})
+			h := mw(func(c echo.Context) error {
+				return nil
+			})
+
+			rec := httptest.NewRecorder()
+			req := httptest.NewRequest(tc.whenMethod, tc.whenURL, nil)
+			c := e.NewContext(req, rec)
+
+			err := h(c)
+			assert.NoError(t, err)
+
+			assert.Equal(t, tc.expectPath, req.URL.Path)
+			assert.Equal(t, tc.expectLocation, rec.Header()[echo.HeaderLocation])
+			if tc.expectStatus == 0 {
+				assert.Equal(t, http.StatusMovedPermanently, rec.Code)
+			} else {
+				assert.Equal(t, tc.expectStatus, rec.Code)
+			}
+		})
+	}
+}
+
 func TestAddTrailingSlash(t *testing.T) {
-	is := assert.New(t)
-	e := echo.New()
-	req := httptest.NewRequest(http.MethodGet, "/add-slash", nil)
-	rec := httptest.NewRecorder()
-	c := e.NewContext(req, rec)
-	h := AddTrailingSlash()(func(c echo.Context) error {
-		return nil
-	})
-	is.NoError(h(c))
-	is.Equal("/add-slash/", req.URL.Path)
-	is.Equal("/add-slash/", req.RequestURI)
-
-	// Method Connect must not fail:
-	req = httptest.NewRequest(http.MethodConnect, "", nil)
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec)
-	h = AddTrailingSlash()(func(c echo.Context) error {
-		return nil
-	})
-	is.NoError(h(c))
-	is.Equal("/", req.URL.Path)
-	is.Equal("/", req.RequestURI)
-
-	// With config
-	req = httptest.NewRequest(http.MethodGet, "/add-slash?key=value", nil)
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec)
-	h = AddTrailingSlashWithConfig(TrailingSlashConfig{
-		RedirectCode: http.StatusMovedPermanently,
-	})(func(c echo.Context) error {
-		return nil
-	})
-	is.NoError(h(c))
-	is.Equal(http.StatusMovedPermanently, rec.Code)
-	is.Equal("/add-slash/?key=value", rec.Header().Get(echo.HeaderLocation))
+	var testCases = []struct {
+		whenURL        string
+		whenMethod     string
+		expectPath     string
+		expectLocation []string
+	}{
+		{
+			whenURL:    "/add-slash",
+			whenMethod: http.MethodGet,
+			expectPath: "/add-slash/",
+		},
+		{
+			whenURL:    "/add-slash?key=value",
+			whenMethod: http.MethodGet,
+			expectPath: "/add-slash/",
+		},
+		{
+			whenURL:        "/",
+			whenMethod:     http.MethodConnect,
+			expectPath:     "/",
+			expectLocation: nil,
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.whenURL, func(t *testing.T) {
+			e := echo.New()
+
+			h := AddTrailingSlash()(func(c echo.Context) error {
+				return nil
+			})
+
+			rec := httptest.NewRecorder()
+			req := httptest.NewRequest(tc.whenMethod, tc.whenURL, nil)
+			c := e.NewContext(req, rec)
+
+			err := h(c)
+			assert.NoError(t, err)
+
+			assert.Equal(t, tc.expectPath, req.URL.Path)
+			assert.Equal(t, []string(nil), rec.Header()[echo.HeaderLocation])
+			assert.Equal(t, http.StatusOK, rec.Code)
+		})
+	}
+}
+
+func TestRemoveTrailingSlashWithConfig(t *testing.T) {
+	var testCases = []struct {
+		whenURL        string
+		whenMethod     string
+		expectPath     string
+		expectLocation []string
+		expectStatus   int
+	}{
+		{
+			whenURL:        "/remove-slash/",
+			whenMethod:     http.MethodGet,
+			expectPath:     "/remove-slash/",
+			expectLocation: []string{`/remove-slash`},
+		},
+		{
+			whenURL:        "/remove-slash/?key=value",
+			whenMethod:     http.MethodGet,
+			expectPath:     "/remove-slash/",
+			expectLocation: []string{`/remove-slash?key=value`},
+		},
+		{
+			whenURL:        "/",
+			whenMethod:     http.MethodConnect,
+			expectPath:     "/",
+			expectLocation: nil,
+			expectStatus:   http.StatusOK,
+		},
+		{
+			whenURL:        "http://localhost",
+			whenMethod:     http.MethodGet,
+			expectPath:     "",
+			expectLocation: nil,
+			expectStatus:   http.StatusOK,
+		},
+		// cases for open redirect vulnerability
+		{
+			whenURL:        "http://localhost:1323/%5Cexample.com/",
+			expectPath:     `/\example.com/`,
+			expectLocation: []string{`/example.com`},
+		},
+		{
+			whenURL:        `http://localhost:1323/\example.com/`,
+			expectPath:     `/\example.com/`,
+			expectLocation: []string{`/example.com`},
+		},
+		{
+			whenURL:        `http://localhost:1323/\\%5C////%5C\\\example.com/`,
+			expectPath:     `/\\\////\\\\example.com/`,
+			expectLocation: []string{`/example.com`},
+		},
+		{
+			whenURL:        "http://localhost:1323//example.com/",
+			expectPath:     `//example.com/`,
+			expectLocation: []string{`/example.com`},
+		},
+		{
+			whenURL:        "http://localhost:1323/%5C%5C/",
+			expectPath:     `/\\/`,
+			expectLocation: []string{`/`},
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.whenURL, func(t *testing.T) {
+			e := echo.New()
+
+			mw := RemoveTrailingSlashWithConfig(TrailingSlashConfig{
+				RedirectCode: http.StatusMovedPermanently,
+			})
+			h := mw(func(c echo.Context) error {
+				return nil
+			})
+
+			rec := httptest.NewRecorder()
+			req := httptest.NewRequest(tc.whenMethod, tc.whenURL, nil)
+			c := e.NewContext(req, rec)
+
+			err := h(c)
+			assert.NoError(t, err)
+
+			assert.Equal(t, tc.expectPath, req.URL.Path)
+			assert.Equal(t, tc.expectLocation, rec.Header()[echo.HeaderLocation])
+			if tc.expectStatus == 0 {
+				assert.Equal(t, http.StatusMovedPermanently, rec.Code)
+			} else {
+				assert.Equal(t, tc.expectStatus, rec.Code)
+			}
+		})
+	}
 }
 
 func TestRemoveTrailingSlash(t *testing.T) {
-	is := assert.New(t)
-	e := echo.New()
-	req := httptest.NewRequest(http.MethodGet, "/remove-slash/", nil)
-	rec := httptest.NewRecorder()
-	c := e.NewContext(req, rec)
-	h := RemoveTrailingSlash()(func(c echo.Context) error {
-		return nil
-	})
-	is.NoError(h(c))
-	is.Equal("/remove-slash", req.URL.Path)
-	is.Equal("/remove-slash", req.RequestURI)
-
-	// Method Connect must not fail:
-	req = httptest.NewRequest(http.MethodConnect, "", nil)
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec)
-	h = RemoveTrailingSlash()(func(c echo.Context) error {
-		return nil
-	})
-	is.NoError(h(c))
-	is.Equal("", req.URL.Path)
-	is.Equal("", req.RequestURI)
-
-	// With config
-	req = httptest.NewRequest(http.MethodGet, "/remove-slash/?key=value", nil)
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec)
-	h = RemoveTrailingSlashWithConfig(TrailingSlashConfig{
-		RedirectCode: http.StatusMovedPermanently,
-	})(func(c echo.Context) error {
-		return nil
-	})
-	is.NoError(h(c))
-	is.Equal(http.StatusMovedPermanently, rec.Code)
-	is.Equal("/remove-slash?key=value", rec.Header().Get(echo.HeaderLocation))
-
-	// With bare URL
-	req = httptest.NewRequest(http.MethodGet, "http://localhost", nil)
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec)
-	h = RemoveTrailingSlash()(func(c echo.Context) error {
-		return nil
-	})
-	is.NoError(h(c))
-	is.Equal("", req.URL.Path)
+	var testCases = []struct {
+		whenURL    string
+		whenMethod string
+		expectPath string
+	}{
+		{
+			whenURL:    "/remove-slash/",
+			whenMethod: http.MethodGet,
+			expectPath: "/remove-slash",
+		},
+		{
+			whenURL:    "/remove-slash/?key=value",
+			whenMethod: http.MethodGet,
+			expectPath: "/remove-slash",
+		},
+		{
+			whenURL:    "/",
+			whenMethod: http.MethodConnect,
+			expectPath: "/",
+		},
+		{
+			whenURL:    "http://localhost",
+			whenMethod: http.MethodGet,
+			expectPath: "",
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.whenURL, func(t *testing.T) {
+			e := echo.New()
+
+			h := RemoveTrailingSlash()(func(c echo.Context) error {
+				return nil
+			})
+
+			rec := httptest.NewRecorder()
+			req := httptest.NewRequest(tc.whenMethod, tc.whenURL, nil)
+			c := e.NewContext(req, rec)
+
+			err := h(c)
+			assert.NoError(t, err)
+
+			assert.Equal(t, tc.expectPath, req.URL.Path)
+			assert.Equal(t, []string(nil), rec.Header()[echo.HeaderLocation])
+			assert.Equal(t, http.StatusOK, rec.Code)
+		})
+	}
 }