Repository: labstack/echo
Pull request #2126 (2022-03-13 14:03:27)

Timeout middleware write race
Timeout middleware occasionally fails with data race in Github CI flow. I have taken code from `v5` branch which has a little bit reworked logic so we are not using `t.ctx.Error(err)` anymore

```text
==================
WARNING: DATA RACE
Read at 0x00c0001e8ca8 by goroutine 70:
  bytes.(*Buffer).String()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/bytes/buffer.go:65 +0x3ca
  github.com/labstack/echo/v4/middleware.TestTimeoutWithFullEchoStack.func2()
      /Users/runner/work/echo/echo/middleware/timeout_test.go:408 +0x3b9
  testing.tRunner()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/testing/testing.go:1123 +0x202

Previous write at 0x00c0001e8ca8 by goroutine 76:
  bytes.(*Buffer).grow()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/bytes/buffer.go:147 +0x2d7
  bytes.(*Buffer).Write()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/bytes/buffer.go:172 +0x184
  github.com/labstack/echo/v4/middleware.LoggerWithConfig.func2.1()
      /Users/runner/work/echo/echo/middleware/logger.go:216 +0x724
  github.com/labstack/echo/v4/middleware.echoHandlerFuncWrapper.ServeHTTP()
      /Users/runner/work/echo/echo/middleware/timeout.go:156 +0x22c
  github.com/labstack/echo/v4/middleware.(*echoHandlerFuncWrapper).ServeHTTP()
      <autogenerated>:1 +0xcc
  net/http.(*timeoutHandler).ServeHTTP.func1()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/net/http/server.go:[32](https://github.com/labstack/echo/runs/5527738819?check_suite_focus=true#step:7:32)72 +0xb5

Goroutine 70 (running) created at:
  testing.(*T).Run()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/testing/testing.go:1168 +0x5bb
  github.com/labstack/echo/v4/middleware.TestTimeoutWithFullEchoStack()
      /Users/runner/work/echo/echo/middleware/timeout_test.go:392 +0xa3b
  testing.tRunner()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/testing/testing.go:1123 +0x202

Goroutine 76 (finished) created at:
  net/http.(*timeoutHandler).ServeHTTP()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/net/http/server.go:3266 +0x38f
  github.com/labstack/echo/v4/middleware.TimeoutWithConfig.func1.1()
      /Users/runner/work/echo/echo/middleware/timeout.go:118 +0x449
  github.com/labstack/echo/v4.(*Echo).ServeHTTP()
      /Users/runner/work/echo/echo/echo.go:630 +0x1fd
  net/http.serverHandler.ServeHTTP()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/net/http/server.go:2843 +0xca
  net/http.(*conn).serve()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/net/http/server.go:1925 +0x84c
==================
==================
WARNING: DATA RACE
Read at 0x00c0001e8c90 by goroutine 70:
  bytes.(*Buffer).String()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/bytes/buffer.go:65 +0x3e4
  github.com/labstack/echo/v4/middleware.TestTimeoutWithFullEchoStack.func2()
      /Users/runner/work/echo/echo/middleware/timeout_test.go:408 +0x3b9
  testing.tRunner()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/testing/testing.go:1123 +0x202

Previous write at 0x00c0001e8c90 by goroutine 76:
  bytes.(*Buffer).grow()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/bytes/buffer.go:144 +0x297
  bytes.(*Buffer).Write()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/bytes/buffer.go:172 +0x184
  github.com/labstack/echo/v4/middleware.LoggerWithConfig.func2.1()
      /Users/runner/work/echo/echo/middleware/logger.go:216 +0x724
  github.com/labstack/echo/v4/middleware.echoHandlerFuncWrapper.ServeHTTP()
      /Users/runner/work/echo/echo/middleware/timeout.go:156 +0x22c
  github.com/labstack/echo/v4/middleware.(*echoHandlerFuncWrapper).ServeHTTP()
      <autogenerated>:1 +0xcc
  net/http.(*timeoutHandler).ServeHTTP.func1()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/net/http/server.go:3272 +0xb5

Goroutine 70 (running) created at:
  testing.(*T).Run()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/testing/testing.go:1168 +0x5bb
  github.com/labstack/echo/v4/middleware.TestTimeoutWithFullEchoStack()
      /Users/runner/work/echo/echo/middleware/timeout_test.go:392 +0xa3b
  testing.tRunner()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/testing/testing.go:1123 +0x202

Goroutine 76 (finished) created at:
  net/http.(*timeoutHandler).ServeHTTP()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/net/http/server.go:3266 +0x38f
  github.com/labstack/echo/v4/middleware.TimeoutWithConfig.func1.1()
      /Users/runner/work/echo/echo/middleware/timeout.go:118 +0x449
  github.com/labstack/echo/v4.(*Echo).ServeHTTP()
      /Users/runner/work/echo/echo/echo.go:630 +0x1fd
  net/http.serverHandler.ServeHTTP()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/net/http/server.go:2843 +0xca
  net/http.(*conn).serve()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/net/http/server.go:1925 +0x84c
==================
==================
WARNING: DATA RACE
Read at 0x00c000188000 by goroutine 70:
  runtime.slicebytetostring()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/runtime/string.go:80 +0x0
  bytes.(*Buffer).String()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/bytes/buffer.go:65 +0x437
  github.com/labstack/echo/v4/middleware.TestTimeoutWithFullEchoStack.func2()
      /Users/runner/work/echo/echo/middleware/timeout_test.go:408 +0x3b9
  testing.tRunner()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/testing/testing.go:1123 +0x202

Previous write at 0x00c000188000 by goroutine 76:
  runtime.slicecopy()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/runtime/slice.go:246 +0x0
  bytes.(*Buffer).Write()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/bytes/buffer.go:174 +0x147
  github.com/labstack/echo/v4/middleware.LoggerWithConfig.func2.1()
      /Users/runner/work/echo/echo/middleware/logger.go:216 +0x724
  github.com/labstack/echo/v4/middleware.echoHandlerFuncWrapper.ServeHTTP()
      /Users/runner/work/echo/echo/middleware/timeout.go:156 +0x22c
  github.com/labstack/echo/v4/middleware.(*echoHandlerFuncWrapper).ServeHTTP()
      <autogenerated>:1 +0xcc
  net/http.(*timeoutHandler).ServeHTTP.func1()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/net/http/server.go:3272 +0xb5

Goroutine 70 (running) created at:
  testing.(*T).Run()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/testing/testing.go:1168 +0x5bb
  github.com/labstack/echo/v4/middleware.TestTimeoutWithFullEchoStack()
      /Users/runner/work/echo/echo/middleware/timeout_test.go:392 +0xa3b
  testing.tRunner()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/testing/testing.go:1123 +0x202

Goroutine 76 (finished) created at:
  net/http.(*timeoutHandler).ServeHTTP()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/net/http/server.go:3266 +0x38f
  github.com/labstack/echo/v4/middleware.TimeoutWithConfig.func1.1()
      /Users/runner/work/echo/echo/middleware/timeout.go:118 +0x449
  github.com/labstack/echo/v4.(*Echo).ServeHTTP()
      /Users/runner/work/echo/echo/echo.go:630 +0x1fd
  net/http.serverHandler.ServeHTTP()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/net/http/server.go:2843 +0xca
  net/http.(*conn).serve()
      /Users/runner/hostedtoolcache/go/1.15.15/x64/src/net/http/server.go:1925 +0x84c
==================
--- FAIL: TestTimeoutWithFullEchoStack (0.12s)
    --- FAIL: TestTimeoutWithFullEchoStack/503_-_handler_timeouts,_write_response_in_timeout_middleware (0.10s)
        timeout_test.go:413: 
            	Error Trace:	timeout_test.go:413
            	Error:      	Should be false
            	Test:       	TestTimeoutWithFullEchoStack/503_-_handler_timeouts,_write_response_in_timeout_middleware
Error:         testing.go:1038: race detected during execution of test
Error:     testing.go:1038: race detected during execution of test
{"time":"2022-03-13T13:06:20.137835Z","level":"-","prefix":"echo","file":"recover.go","line":"109","message":"[PANIC RECOVER] panic!!! goroutine 494 [running]:\ngithub.com/labstack/echo/v4/middleware.RecoverWithConfig.func1.1.1(0x19e6070, 0x1000, 0x0, 0x0, 0x1ab46c0, 0xc0001d6820)\n\t/Users/runner/work/echo/echo/middleware/recover.go:89 +0xa0f\npanic(0x18f3f00, 0x1a8ff90)\n\t/Users/runner/hostedtoolcache/go/1.15.15/x64/src/runtime/panic.go:975 +0x47a\nnet/http.(*timeoutHandler).ServeHTTP(0xc0000f3e40, 0x1aa7f80, 0xc0000f3dc0, 0xc0003ff900)\n\t/Users/runner/hostedtoolcache/go/1.15.15/x64/src/net/http/server.go:3277 +0xe8b\ngithub.com/labstack/echo/v4/middleware.TimeoutWithConfig.func1.1(0x1ab46c0, 0xc0001d6820, 0xc00000000c, 0xc00018de00)\n\t/Users/runner/work/echo/echo/middleware/timeout.go:118 +0x44a\ngithub.com/labstack/echo/v4/middleware.RecoverWithConfig.func1.1(0x1ab46c0, 0xc0001d6820, 0x0, 0x0)\n\t/Users/runner/work/echo/echo/middleware/recover.go:115 +0x1ab\ngithub.com/labstack/echo/v4.(*Echo).ServeHTTP(0xc0006c2480, 0x1aa7f80, 0xc0000f3dc0, 0xc0003ff900)\n\t/Users/runner/work/echo/echo/echo.go:630 +0x1fe\ngithub.com/labstack/echo/v4/middleware.TestTimeoutRecoversPanic.func2()\n\t/Users/runner/work/echo/echo/middleware/timeout_test.go:190 +0x65\ngithub.com/stretchr/testify/assert.didPanic.func1(0xc00005bd08, 0xc00005bcd6, 0xc00005bcf8, 0xc000666ee0)\n\t/Users/runner/work/echo/pkg/mod/github.com/stretchr/testify@v1.7.0/assert/assertions.go:1018 +0x8c\ngithub.com/stretchr/testify/assert.didPanic(0xc000666ee0, 0x1a9e5a0, 0xc0002ec300, 0x2fd9ad8, 0xc0002ec300, 0xc00005bd01)\n\t/Users/runner/work/echo/pkg/mod/github.com/stretchr/testify@v1.7.0/assert/assertions.go:1020 +0x6d\ngithub.com/stretchr/testify/assert.NotPanics(0x1a9e5a0, 0xc0002ec300, 0xc000666ee0, 0x0, 0x0, 0x0, 0xc0003ff900)\n\t/Users/runner/work/echo/pkg/mod/github.com/stretchr/testify@v1.7.0/assert/assertions.go:1091 +0x85\ngithub.com/labstack/echo/v4/middleware.TestTimeoutRecoversPanic(0xc0002ec300)\n\t/Users/runner/work/echo/echo/middleware/timeout_test.go:189 +0x57b\ntesting.tRunner(0xc0002ec300, 0x19e68d0)\n\t/Users/runner/hostedtoolcache/go/1.15.15/x64/src/testing/testing.go:1123 +0x203\ncreated by testing.(*T).Run\n\t/Users/runner/hostedtoolcache/go/1.15.15/x64/src/testing/testing.go:1168 +0x5bc\n\ngoroutine 1 [chan receive]:\ntesting.tRunner.func1(0xc000082900)\n\t/Users/runner/hostedtoolcache/go/1.15.15/x64/src/testing/testing.go:1088 +0x[33](https://github.com/labstack/echo/runs/5527738819?check_suite_focus=true#step:7:33)3\ntesting.tRunner(0xc000082900, 0xc0001b1c78)\n\t/Users/runner/hostedtoolcache/go/1.15.15/x64/src/testing/testing.go:1127 +0x22b\ntesting.runTests(0xc0000b0b40, 0x1e32f60, 0x7d, 0x7d, 0xc0839988da1e9988, 0x8bb30c1c78, 0x1e378e0, 0xc0001c8190)\n\t/Users/runner/hostedtoolcache/go/1.15.15/x64/src/testing/testing.go:1437 +0x613\ntesting.(*M).Run(0xc00018c180, 0x0)\n\t/Users/runner/hostedtoolcache/go/1.15.15/x64/src/testing/testing.go:1[34](https://github.com/labstack/echo/runs/5527738819?check_suite_focus=true#step:7:34)5 +0x3b4\nmain.main()\n\t_testmain.go:407 +0x[35](https://github.com/labstack/echo/runs/5527738819?check_suite_focus=true#step:7:35)7\n\ngoroutine 215 [select]:\nnet/http.(*persistConn).writeLoop(0xc00013fe60)\n\t/Users/runner/hostedtoolcache/go/1.15.15/x64/src/net/http/transport.go:2346 +0x1d4\ncreated by net/http.(*Transport).dialConn\n\t/Users/runner/hostedtoolcache/go/1.15.15/x64/src/net/http/transport.go:1716 +0xc31\n\ngoroutine 214 [select]:\nnet/http.(*persistConn).readLoop(0xc00013fe60)\n\t/Users/runner/hostedtoolcache/go/1.15.15/x64/src/net/http/transport.go:2167 +0xf3b\ncreated by net/http.(*Transport).dialConn\n\t/Users/runner/hostedtoolcache/go/1.15.15/x64/src/net/http/transport.go:1715 +0xc0c\n\ngoroutine 495 [chan send]:\ntesting.tRunner.func1(0xc0002ec480)\n\t/Users/runner/hostedtoolcache/go/1.15.15/x64/src/testing/testing.go:1113 +0x505\ntesting.tRunner(0xc0002ec480, 0x19e68a8)\n\t/Users/runner/hostedtoolcache/go/1.15.15/x64/src/testing/testing.go:1127 +0x22b\ncreated by testing.(*T).Run\n\t/Users/runner/hostedtoolcache/go/1.15.15/x64/src/testing/testing.go:1168 +0x5bc\n\ngoroutine 496 [select]:\nnet/http.(*timeoutHandler).ServeHTTP(0xc0000f3d80, 0x1aa7f80, 0xc0000f3d00, 0xc0003ff700)\n\t/Users/runner/hostedtoolcache/go/1.15.15/x64/src/net/http/server.go:3275 +0x4bf\ngithub.com/labstack/echo/v4/middleware.TimeoutWithConfig.func1.1(0x1ab46c0, 0xc0001d66e0, 0xc000034f60, 0x1e68[39](https://github.com/labstack/echo/runs/5527738819?check_suite_focus=true#step:7:39)0)\n\t/Users/runner/work/echo/echo/middleware/timeout.go:118 +0x[44](https://github.com/labstack/echo/runs/5527738819?check_suite_focus=true#step:7:44)a\ngithub.com/labstack/echo/v4/\n"}
FAIL
coverage: [92](https://github.com/labstack/echo/runs/5527738819?check_suite_focus=true#step:7:92).1% of statements
FAIL	github.com/labstack/echo/v4/middleware	1.002s
```

codecov[bot]: # [Codecov](https://codecov.io/gh/labstack/echo/pull/2126?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) Report
> Merging [#2126](https://codecov.io/gh/labstack/echo/pull/2126?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) (5436ee9) into [master](https://codecov.io/gh/labstack/echo/commit/3f5b733425617138573e3768381278f619561f7e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) (3f5b733) will **decrease** coverage by `0.01%`.
> The diff coverage is `91.66%`.

[![Impacted file tree graph](https://codecov.io/gh/labstack/echo/pull/2126/graphs/tree.svg?width=650&height=150&src=pr&token=vqnT76l79i&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack)](https://codecov.io/gh/labstack/echo/pull/2126?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack)

```diff
@@            Coverage Diff             @@
##           master    #2126      +/-   ##
==========================================
- Coverage   92.10%   92.09%   -0.02%     
==========================================
  Files          37       37              
  Lines        3028     3047      +19     
==========================================
+ Hits         2789     2806      +17     
- Misses        150      151       +1     
- Partials       89       90       +1     
```


| [Impacted Files](https://codecov.io/gh/labstack/echo/pull/2126?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) | Coverage Δ | |
|---|---|---|
| [middleware/timeout.go](https://codecov.io/gh/labstack/echo/pull/2126/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack#diff-bWlkZGxld2FyZS90aW1lb3V0Lmdv) | `96.42% <91.66%> (-3.58%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/labstack/echo/pull/2126?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/labstack/echo/pull/2126?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack). Last update [3f5b733...5436ee9](https://codecov.io/gh/labstack/echo/pull/2126?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 461ac89..7d1d908 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
 # Changelog
 
+## v4.7.1 - 2022-03-13
+
+**Fixes**
+
+* Fix `e.Static`, `.File()`, `c.Attachment()` being picky with paths starting with `./`, `../` and `/` after 4.7.0 introduced echo.Filesystem support (Go1.16+) [#2123](https://github.com/labstack/echo/pull/2123)
+
+**Enhancements**
+
+* Remove some unused code [#2116](https://github.com/labstack/echo/pull/2116)
+
+
 ## v4.7.0 - 2022-03-01
 
 **Enhancements**
diff --git a/echo.go b/echo.go
index 143f9ff..c2f23c1 100644
--- a/echo.go
+++ b/echo.go
@@ -246,7 +246,7 @@ const (
 
 const (
 	// Version of Echo
-	Version = "4.7.0"
+	Version = "4.7.1"
 	website = "https://echo.labstack.com"
 	// http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo
 	banner = `
@@ -732,7 +732,7 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
 	return s.Serve(e.Listener)
 }
 
-func (e *Echo) configureServer(s *http.Server) (err error) {
+func (e *Echo) configureServer(s *http.Server) error {
 	// Setup
 	e.colorer.SetOutput(e.Logger.Output())
 	s.ErrorLog = e.StdLogger
@@ -747,10 +747,11 @@ func (e *Echo) configureServer(s *http.Server) (err error) {
 
 	if s.TLSConfig == nil {
 		if e.Listener == nil {
-			e.Listener, err = newListener(s.Addr, e.ListenerNetwork)
+			l, err := newListener(s.Addr, e.ListenerNetwork)
 			if err != nil {
 				return err
 			}
+			e.Listener = l
 		}
 		if !e.HidePort {
 			e.colorer.Printf("⇨ http server started on %s\n", e.colorer.Green(e.Listener.Addr()))
@@ -791,7 +792,7 @@ func (e *Echo) TLSListenerAddr() net.Addr {
 }
 
 // StartH2CServer starts a custom http/2 server with h2c (HTTP/2 Cleartext).
-func (e *Echo) StartH2CServer(address string, h2s *http2.Server) (err error) {
+func (e *Echo) StartH2CServer(address string, h2s *http2.Server) error {
 	e.startupMutex.Lock()
 	// Setup
 	s := e.Server
@@ -808,11 +809,12 @@ func (e *Echo) StartH2CServer(address string, h2s *http2.Server) (err error) {
 	}
 
 	if e.Listener == nil {
-		e.Listener, err = newListener(s.Addr, e.ListenerNetwork)
+		l, err := newListener(s.Addr, e.ListenerNetwork)
 		if err != nil {
 			e.startupMutex.Unlock()
 			return err
 		}
+		e.Listener = l
 	}
 	if !e.HidePort {
 		e.colorer.Printf("⇨ http server started on %s\n", e.colorer.Green(e.Listener.Addr()))
diff --git a/middleware/extractor.go b/middleware/extractor.go
index a57ed4e..afdfd81 100644
--- a/middleware/extractor.go
+++ b/middleware/extractor.go
@@ -168,8 +168,8 @@ func valuesFromCookie(name string) ValuesExtractor {
 // valuesFromForm returns a function that extracts values from the form field.
 func valuesFromForm(name string) ValuesExtractor {
 	return func(c echo.Context) ([]string, error) {
-		if parseErr := c.Request().ParseForm(); parseErr != nil {
-			return nil, fmt.Errorf("valuesFromForm parse form failed: %w", parseErr)
+		if c.Request().Form == nil {
+			_ = c.Request().ParseMultipartForm(32 << 20) // same what `c.Request().FormValue(name)` does
 		}
 		values := c.Request().Form[name]
 		if len(values) == 0 {
diff --git a/middleware/recover.go b/middleware/recover.go
index a621a9e..7b61285 100644
--- a/middleware/recover.go
+++ b/middleware/recover.go
@@ -2,6 +2,7 @@ package middleware
 
 import (
 	"fmt"
+	"net/http"
 	"runtime"
 
 	"github.com/labstack/echo/v4"
@@ -77,6 +78,9 @@ func RecoverWithConfig(config RecoverConfig) echo.MiddlewareFunc {
 
 			defer func() {
 				if r := recover(); r != nil {
+					if r == http.ErrAbortHandler {
+						panic(r)
+					}
 					err, ok := r.(error)
 					if !ok {
 						err = fmt.Errorf("%v", r)
diff --git a/middleware/timeout.go b/middleware/timeout.go
index 768ef8d..4e8836c 100644
--- a/middleware/timeout.go
+++ b/middleware/timeout.go
@@ -2,10 +2,10 @@ package middleware
 
 import (
 	"context"
+	"github.com/labstack/echo/v4"
 	"net/http"
+	"sync"
 	"time"
-
-	"github.com/labstack/echo/v4"
 )
 
 // ---------------------------------------------------------------------------------------------------------------
@@ -55,29 +55,27 @@ import (
 //	})
 //
 
-type (
-	// TimeoutConfig defines the config for Timeout middleware.
-	TimeoutConfig struct {
-		// Skipper defines a function to skip middleware.
-		Skipper Skipper
-
-		// ErrorMessage is written to response on timeout in addition to http.StatusServiceUnavailable (503) status code
-		// It can be used to define a custom timeout error message
-		ErrorMessage string
-
-		// OnTimeoutRouteErrorHandler is an error handler that is executed for error that was returned from wrapped route after
-		// request timeouted and we already had sent the error code (503) and message response to the client.
-		// NB: do not write headers/body inside this handler. The response has already been sent to the client and response writer
-		// will not accept anything no more. If you want to know what actual route middleware timeouted use `c.Path()`
-		OnTimeoutRouteErrorHandler func(err error, c echo.Context)
-
-		// Timeout configures a timeout for the middleware, defaults to 0 for no timeout
-		// NOTE: when difference between timeout duration and handler execution time is almost the same (in range of 100microseconds)
-		// the result of timeout does not seem to be reliable - could respond timeout, could respond handler output
-		// difference over 500microseconds (0.5millisecond) response seems to be reliable
-		Timeout time.Duration
-	}
-)
+// TimeoutConfig defines the config for Timeout middleware.
+type TimeoutConfig struct {
+	// Skipper defines a function to skip middleware.
+	Skipper Skipper
+
+	// ErrorMessage is written to response on timeout in addition to http.StatusServiceUnavailable (503) status code
+	// It can be used to define a custom timeout error message
+	ErrorMessage string
+
+	// OnTimeoutRouteErrorHandler is an error handler that is executed for error that was returned from wrapped route after
+	// request timeouted and we already had sent the error code (503) and message response to the client.
+	// NB: do not write headers/body inside this handler. The response has already been sent to the client and response writer
+	// will not accept anything no more. If you want to know what actual route middleware timeouted use `c.Path()`
+	OnTimeoutRouteErrorHandler func(err error, c echo.Context)
+
+	// Timeout configures a timeout for the middleware, defaults to 0 for no timeout
+	// NOTE: when difference between timeout duration and handler execution time is almost the same (in range of 100microseconds)
+	// the result of timeout does not seem to be reliable - could respond timeout, could respond handler output
+	// difference over 500microseconds (0.5millisecond) response seems to be reliable
+	Timeout time.Duration
+}
 
 var (
 	// DefaultTimeoutConfig is the default Timeout middleware config.
@@ -94,10 +92,17 @@ func Timeout() echo.MiddlewareFunc {
 	return TimeoutWithConfig(DefaultTimeoutConfig)
 }
 
-// TimeoutWithConfig returns a Timeout middleware with config.
-// See: `Timeout()`.
+// TimeoutWithConfig returns a Timeout middleware with config or panics on invalid configuration.
 func TimeoutWithConfig(config TimeoutConfig) echo.MiddlewareFunc {
-	// Defaults
+	mw, err := config.ToMiddleware()
+	if err != nil {
+		panic(err)
+	}
+	return mw
+}
+
+// ToMiddleware converts Config to middleware or returns an error for invalid configuration
+func (config TimeoutConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
 	if config.Skipper == nil {
 		config.Skipper = DefaultTimeoutConfig.Skipper
 	}
@@ -108,26 +113,29 @@ func TimeoutWithConfig(config TimeoutConfig) echo.MiddlewareFunc {
 				return next(c)
 			}
 
+			errChan := make(chan error, 1)
 			handlerWrapper := echoHandlerFuncWrapper{
+				writer:     &ignorableWriter{ResponseWriter: c.Response().Writer},
 				ctx:        c,
 				handler:    next,
-				errChan:    make(chan error, 1),
+				errChan:    errChan,
 				errHandler: config.OnTimeoutRouteErrorHandler,
 			}
 			handler := http.TimeoutHandler(handlerWrapper, config.Timeout, config.ErrorMessage)
-			handler.ServeHTTP(c.Response().Writer, c.Request())
+			handler.ServeHTTP(handlerWrapper.writer, c.Request())
 
 			select {
-			case err := <-handlerWrapper.errChan:
+			case err := <-errChan:
 				return err
 			default:
 				return nil
 			}
 		}
-	}
+	}, nil
 }
 
 type echoHandlerFuncWrapper struct {
+	writer     *ignorableWriter
 	ctx        echo.Context
 	handler    echo.HandlerFunc
 	errHandler func(err error, c echo.Context)
@@ -160,23 +168,53 @@ func (t echoHandlerFuncWrapper) ServeHTTP(rw http.ResponseWriter, r *http.Reques
 		}
 		return // on timeout we can not send handler error to client because `http.TimeoutHandler` has already sent headers
 	}
-	// we restore original writer only for cases we did not timeout. On timeout we have already sent response to client
-	// and should not anymore send additional headers/data
-	// so on timeout writer stays what http.TimeoutHandler uses and prevents writing headers/body
 	if err != nil {
-		// Error must be written into Writer created in `http.TimeoutHandler` so to get Response into `commited` state.
-		// So call global error handler to write error to the client. This is needed or `http.TimeoutHandler` will send
-		// status code by itself and after that our tries to write status code will not work anymore and/or create errors in
-		// log about `superfluous response.WriteHeader call from`
-		t.ctx.Error(err)
-		// we pass error from handler to middlewares up in handler chain to act on it if needed. But this means that
-		// global error handler is probably be called twice as `t.ctx.Error` already does that.
-
-		// NB: later call of the global error handler or middlewares will not take any effect, as echo.Response will be
-		// already marked as `committed` because we called global error handler above.
-		t.ctx.Response().Writer = originalWriter // make sure we restore before we signal original coroutine about the error
+		// This is needed as `http.TimeoutHandler` will write status code by itself on error and after that our tries to write
+		// status code will not work anymore as Echo.Response thinks it has been already "committed" and further writes
+		// create errors in log about `superfluous response.WriteHeader call from`
+		t.writer.Ignore(true)
+		t.ctx.Response().Writer = originalWriter // make sure we restore writer before we signal original coroutine about the error
+		// we pass error from handler to middlewares up in handler chain to act on it if needed.
 		t.errChan <- err
 		return
 	}
+	// we restore original writer only for cases we did not timeout. On timeout we have already sent response to client
+	// and should not anymore send additional headers/data
+	// so on timeout writer stays what http.TimeoutHandler uses and prevents writing headers/body
 	t.ctx.Response().Writer = originalWriter
 }
+
+// ignorableWriter is ResponseWriter implementations that allows us to mark writer to ignore further write calls. This
+// is handy in cases when you do not have direct control of code being executed (3rd party middleware) but want to make
+// sure that external code will not be able to write response to the client.
+// Writer is coroutine safe for writes.
+type ignorableWriter struct {
+	http.ResponseWriter
+
+	lock         sync.Mutex
+	ignoreWrites bool
+}
+
+func (w *ignorableWriter) Ignore(ignore bool) {
+	w.lock.Lock()
+	w.ignoreWrites = ignore
+	w.lock.Unlock()
+}
+
+func (w *ignorableWriter) WriteHeader(code int) {
+	w.lock.Lock()
+	defer w.lock.Unlock()
+	if w.ignoreWrites {
+		return
+	}
+	w.ResponseWriter.WriteHeader(code)
+}
+
+func (w *ignorableWriter) Write(b []byte) (int, error) {
+	w.lock.Lock()
+	defer w.lock.Unlock()
+	if w.ignoreWrites {
+		return len(b), nil
+	}
+	return w.ResponseWriter.Write(b)
+}


Test patch:
diff --git a/middleware/extractor_test.go b/middleware/extractor_test.go
index ae4b30a..2e898f5 100644
--- a/middleware/extractor_test.go
+++ b/middleware/extractor_test.go
@@ -1,9 +1,11 @@
 package middleware
 
 import (
+	"bytes"
 	"fmt"
 	"github.com/labstack/echo/v4"
 	"github.com/stretchr/testify/assert"
+	"mime/multipart"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -499,6 +501,25 @@ func TestValuesFromForm(t *testing.T) {
 		return req
 	}
 
+	exampleMultiPartFormRequest := func(mod func(w *multipart.Writer)) *http.Request {
+		var b bytes.Buffer
+		w := multipart.NewWriter(&b)
+		w.WriteField("name", "Jon Snow")
+		w.WriteField("emails[]", "jon@labstack.com")
+		if mod != nil {
+			mod(w)
+		}
+
+		fw, _ := w.CreateFormFile("upload", "my.file")
+		fw.Write([]byte(`<div>hi</div>`))
+		w.Close()
+
+		req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(b.String()))
+		req.Header.Add(echo.HeaderContentType, w.FormDataContentType())
+
+		return req
+	}
+
 	var testCases = []struct {
 		name         string
 		givenRequest *http.Request
@@ -520,6 +541,14 @@ func TestValuesFromForm(t *testing.T) {
 			whenName:     "emails[]",
 			expectValues: []string{"jon@labstack.com", "snow@labstack.com"},
 		},
+		{
+			name: "ok, POST multipart/form, multiple value",
+			givenRequest: exampleMultiPartFormRequest(func(w *multipart.Writer) {
+				w.WriteField("emails[]", "snow@labstack.com")
+			}),
+			whenName:     "emails[]",
+			expectValues: []string{"jon@labstack.com", "snow@labstack.com"},
+		},
 		{
 			name:         "ok, GET form, single value",
 			givenRequest: exampleGetFormRequest(nil),
@@ -540,16 +569,6 @@ func TestValuesFromForm(t *testing.T) {
 			whenName:     "nope",
 			expectError:  errFormExtractorValueMissing.Error(),
 		},
-		{
-			name: "nok, POST form, form parsing error",
-			givenRequest: func() *http.Request {
-				req := httptest.NewRequest(http.MethodPost, "/", nil)
-				req.Body = nil
-				return req
-			}(),
-			whenName:    "name",
-			expectError: "valuesFromForm parse form failed: missing form body",
-		},
 		{
 			name: "ok, cut values over extractorLimit",
 			givenRequest: examplePostFormRequest(func(v *url.Values) {
diff --git a/middleware/recover_test.go b/middleware/recover_test.go
index 9ac4fee..b27f3b4 100644
--- a/middleware/recover_test.go
+++ b/middleware/recover_test.go
@@ -28,6 +28,35 @@ func TestRecover(t *testing.T) {
 	assert.Contains(t, buf.String(), "PANIC RECOVER")
 }
 
+func TestRecoverErrAbortHandler(t *testing.T) {
+	e := echo.New()
+	buf := new(bytes.Buffer)
+	e.Logger.SetOutput(buf)
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+	h := Recover()(echo.HandlerFunc(func(c echo.Context) error {
+		panic(http.ErrAbortHandler)
+	}))
+	defer func() {
+		r := recover()
+		if r == nil {
+			assert.Fail(t, "expecting `http.ErrAbortHandler`, got `nil`")
+		} else {
+			if err, ok := r.(error); ok {
+				assert.ErrorIs(t, err, http.ErrAbortHandler)
+			} else {
+				assert.Fail(t, "not of error type")
+			}
+		}
+	}()
+
+	h(c)
+
+	assert.Equal(t, http.StatusInternalServerError, rec.Code)
+	assert.NotContains(t, buf.String(), "PANIC RECOVER")
+}
+
 func TestRecoverWithConfig_LogLevel(t *testing.T) {
 	tests := []struct {
 		logLevel  log.Lvl
diff --git a/middleware/timeout_test.go b/middleware/timeout_test.go
index aa6402b..7fb802a 100644
--- a/middleware/timeout_test.go
+++ b/middleware/timeout_test.go
@@ -74,13 +74,18 @@ func TestTimeoutErrorOutInHandler(t *testing.T) {
 	e := echo.New()
 	c := e.NewContext(req, rec)
 
+	rec.Code = 1 // we want to be sure that even 200 will not be sent
 	err := m(func(c echo.Context) error {
+		// this error must not be written to the client response. Middlewares upstream of timeout middleware must be able
+		// to handle returned error and this can be done only then handler has not yet committed (written status code)
+		// the response.
 		return echo.NewHTTPError(http.StatusTeapot, "err")
 	})(c)
 
 	assert.Error(t, err)
-	assert.Equal(t, http.StatusTeapot, rec.Code)
-	assert.Equal(t, "{\"message\":\"err\"}\n", rec.Body.String())
+	assert.EqualError(t, err, "code=418, message=err")
+	assert.Equal(t, 1, rec.Code)
+	assert.Equal(t, "", rec.Body.String())
 }
 
 func TestTimeoutSuccessfulRequest(t *testing.T) {