Repository: traefik/traefik (language: go)

Issue:
HEAD request takes too long time to response
### Welcome!

- [x] Yes, I've searched similar issues on [GitHub](https://github.com/traefik/traefik/issues) and didn't find any.
- [x] Yes, I've searched similar issues on the [Traefik community forum](https://community.traefik.io) and didn't find any.

### What did you do?

- I setup a nginx as static file server & gitea as package manangment
- All staffs runs in docker
- I setup traefik in docker as gateway, version tested: v3.2.3 & 3.3.0-RC2 & 3.3.0
- Test url via `curl http://172.16.x.x/` and `curl -I http://172.16.x.x/`

### What did you see instead?

- **`curl http://172.16.x.x/`**: works ok, responsed in <20ms
- **`curl -I http://172.16.x.x/`**: hangs up for long time, and then:
  - for some url, like nginx static file url, finally got response after 1minute
  - for some url, like docker registery head request with 401 response (via gitea), request hangs up infinitely
- **`curl -I http://172.16.x.x:3000/`**: works ok, reponsed in <20ms (`3000`was the backend port mapped to host)

### What version of Traefik are you using?

**Tested**

- v3.2.4
- v3.3.0-rc2
- v3.3.0

### What is your environment & configuration?

Traefik config on backend container really simple and now extra complex configuration:
```
      - traefik.enable=true
      - traefik.http.routers.nginx.entrypoints=web
      - traefik.http.routers.nginx.rule=Host(`example.com`)
      - traefik.http.routers.nginx.service=nginx
      - traefik.http.services.nginx.loadbalancer.server.port=3000
```


### If applicable, please paste the log output in DEBUG level

No log. If HEAD request hangs up, no access log was shown in terminal.


Discussion / hints:
Found the problem. Enable the fastProxy via CLI `--experimental.fastProxy` caused this issue. Remove this flag the HEAD request works ok.

Fix patch:
diff --git a/pkg/proxy/fast/connpool.go b/pkg/proxy/fast/connpool.go
index 375fac2fb4..4c0be0fe96 100644
--- a/pkg/proxy/fast/connpool.go
+++ b/pkg/proxy/fast/connpool.go
@@ -20,8 +20,9 @@ import (
 // rwWithUpgrade contains a ResponseWriter and an upgradeHandler,
 // used to upgrade the connection (e.g. Websockets).
 type rwWithUpgrade struct {
-	RW      http.ResponseWriter
-	Upgrade upgradeHandler
+	ReqMethod string
+	RW        http.ResponseWriter
+	Upgrade   upgradeHandler
 }
 
 // conn is an enriched net.Conn.
@@ -211,6 +212,10 @@ func (c *conn) handleResponse(r rwWithUpgrade) error {
 
 	r.RW.WriteHeader(res.StatusCode())
 
+	if noResponseBodyExpected(r.ReqMethod) {
+		return nil
+	}
+
 	if res.Header.ContentLength() == 0 {
 		return nil
 	}
@@ -444,8 +449,8 @@ func (c *connPool) askForNewConn(errCh chan<- error) {
 	c.releaseConn(newConn)
 }
 
-// isBodyAllowedForStatus reports whether a given response status code
-// permits a body. See RFC 7230, section 3.3.
+// isBodyAllowedForStatus reports whether a given response status code permits a body.
+// See RFC 7230, section 3.3.
 // From https://github.com/golang/go/blame/master/src/net/http/transfer.go#L459
 func isBodyAllowedForStatus(status int) bool {
 	switch {
@@ -458,3 +463,9 @@ func isBodyAllowedForStatus(status int) bool {
 	}
 	return true
 }
+
+// noResponseBodyExpected reports whether a given request method permits a body.
+// From https://github.com/golang/go/blame/master/src/net/http/transfer.go#L250
+func noResponseBodyExpected(requestMethod string) bool {
+	return requestMethod == "HEAD"
+}
diff --git a/pkg/proxy/fast/proxy.go b/pkg/proxy/fast/proxy.go
index 717b1ff06e..06e68513e4 100644
--- a/pkg/proxy/fast/proxy.go
+++ b/pkg/proxy/fast/proxy.go
@@ -284,8 +284,9 @@ func (p *ReverseProxy) roundTrip(rw http.ResponseWriter, req *http.Request, outR
 
 	// Sending the responseWriter unlocks the connection readLoop, to handle the response.
 	co.RWCh <- rwWithUpgrade{
-		RW:      rw,
-		Upgrade: upgradeResponseHandler(req.Context(), reqUpType),
+		ReqMethod: req.Method,
+		RW:        rw,
+		Upgrade:   upgradeResponseHandler(req.Context(), reqUpType),
 	}
 
 	if err := <-co.ErrCh; err != nil {


Test patch:
diff --git a/pkg/proxy/fast/proxy_test.go b/pkg/proxy/fast/proxy_test.go
index f4593d9ce2..b9f95b6069 100644
--- a/pkg/proxy/fast/proxy_test.go
+++ b/pkg/proxy/fast/proxy_test.go
@@ -278,6 +278,34 @@ func TestPreservePath(t *testing.T) {
 	assert.Equal(t, http.StatusOK, res.Code)
 }
 
+func TestHeadRequest(t *testing.T) {
+	var callCount int
+	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		callCount++
+
+		assert.Equal(t, http.MethodHead, req.Method)
+
+		rw.Header().Set("Content-Length", "42")
+	}))
+	t.Cleanup(server.Close)
+
+	builder := NewProxyBuilder(&transportManagerMock{}, static.FastProxyConfig{})
+
+	serverURL, err := url.JoinPath(server.URL)
+	require.NoError(t, err)
+
+	proxyHandler, err := builder.Build("", testhelpers.MustParseURL(serverURL), true, true)
+	require.NoError(t, err)
+
+	req := httptest.NewRequest(http.MethodHead, "/", http.NoBody)
+	res := httptest.NewRecorder()
+
+	proxyHandler.ServeHTTP(res, req)
+
+	assert.Equal(t, 1, callCount)
+	assert.Equal(t, http.StatusOK, res.Code)
+}
+
 func newCertificate(t *testing.T, domain string) *tls.Certificate {
 	t.Helper()