Repository: traefik/traefik (language: go)

Issue:
Metric "traefik_service_requests_total" is recorded with code="0"
### 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 run Traefik in local. If I add buffering middleware to a service and get metrics, I get  `traefik_service_requests_total` with `code="0"`, while `traefik_entrypoint_requests_total` is recorded with a correct `code`.

### What did you see instead?

`traefik_service_requests_total` should be recoreded with a correct `code`

### What version of Traefik are you using?

2.9.0-rc5

### What is your environment & configuration?

* **static.yml**
```yaml
providers:
  file:
    filename: ./config/dynamic.yml
    watch: true

entryPoints:
  web:
    address: :50090
  metrics:
    address: :9900

metrics:
  prometheus: 
    entryPoint: metrics

api:
  insecure: true
```

* **dynamic.yml**
```yaml
## dynamic configuration ##
http:
  routers:
    test:
      service: test-http
      rule: PathPrefix(`/`)
      middlewares:
        - buffering-15mib

  services:
    test-http:
      loadBalancer:
        servers:
          - url: http://localhost:9500

  middlewares:
    buffering-15mib:
      buffering:
        maxRequestBodyBytes: 15728640 # 15 MiB
        memRequestBodyBytes: 524288   # 512 KiB
```

* **metrics**
```
# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 3.2465e-05
go_gc_duration_seconds{quantile="0.25"} 0.000126169
go_gc_duration_seconds{quantile="0.5"} 0.000149059
go_gc_duration_seconds{quantile="0.75"} 0.000207191
go_gc_duration_seconds{quantile="1"} 0.000213735
go_gc_duration_seconds_sum 0.000728619
go_gc_duration_seconds_count 5
# HELP go_goroutines Number of goroutines that currently exist.
# TYPE go_goroutines gauge
go_goroutines 39
# HELP go_info Information about the Go environment.
# TYPE go_info gauge
go_info{version="go1.19"} 1
# HELP go_memstats_alloc_bytes Number of bytes allocated and still in use.
# TYPE go_memstats_alloc_bytes gauge
go_memstats_alloc_bytes 6.911344e+06
# HELP go_memstats_alloc_bytes_total Total number of bytes allocated, even if freed.
# TYPE go_memstats_alloc_bytes_total counter
go_memstats_alloc_bytes_total 1.9510856e+07
# HELP go_memstats_buck_hash_sys_bytes Number of bytes used by the profiling bucket hash table.
# TYPE go_memstats_buck_hash_sys_bytes gauge
go_memstats_buck_hash_sys_bytes 1.459044e+06
# HELP go_memstats_frees_total Total number of frees.
# TYPE go_memstats_frees_total counter
go_memstats_frees_total 59400
# HELP go_memstats_gc_sys_bytes Number of bytes used for garbage collection system metadata.
# TYPE go_memstats_gc_sys_bytes gauge
go_memstats_gc_sys_bytes 9.79844e+06
# HELP go_memstats_heap_alloc_bytes Number of heap bytes allocated and still in use.
# TYPE go_memstats_heap_alloc_bytes gauge
go_memstats_heap_alloc_bytes 6.911344e+06
# HELP go_memstats_heap_idle_bytes Number of heap bytes waiting to be used.
# TYPE go_memstats_heap_idle_bytes gauge
go_memstats_heap_idle_bytes 1.1255808e+07
# HELP go_memstats_heap_inuse_bytes Number of heap bytes that are in use.
# TYPE go_memstats_heap_inuse_bytes gauge
go_memstats_heap_inuse_bytes 8.962048e+06
# HELP go_memstats_heap_objects Number of allocated objects.
# TYPE go_memstats_heap_objects gauge
go_memstats_heap_objects 32935
# HELP go_memstats_heap_released_bytes Number of heap bytes released to OS.
# TYPE go_memstats_heap_released_bytes gauge
go_memstats_heap_released_bytes 5.472256e+06
# HELP go_memstats_heap_sys_bytes Number of heap bytes obtained from system.
# TYPE go_memstats_heap_sys_bytes gauge
go_memstats_heap_sys_bytes 2.0217856e+07
# HELP go_memstats_last_gc_time_seconds Number of seconds since 1970 of last garbage collection.
# TYPE go_memstats_last_gc_time_seconds gauge
go_memstats_last_gc_time_seconds 1.6646372405005171e+09
# HELP go_memstats_lookups_total Total number of pointer lookups.
# TYPE go_memstats_lookups_total counter
go_memstats_lookups_total 0
# HELP go_memstats_mallocs_total Total number of mallocs.
# TYPE go_memstats_mallocs_total counter
go_memstats_mallocs_total 92335
# HELP go_memstats_mcache_inuse_bytes Number of bytes in use by mcache structures.
# TYPE go_memstats_mcache_inuse_bytes gauge
go_memstats_mcache_inuse_bytes 9600
# HELP go_memstats_mcache_sys_bytes Number of bytes used for mcache structures obtained from system.
# TYPE go_memstats_mcache_sys_bytes gauge
go_memstats_mcache_sys_bytes 15600
# HELP go_memstats_mspan_inuse_bytes Number of bytes in use by mspan structures.
# TYPE go_memstats_mspan_inuse_bytes gauge
go_memstats_mspan_inuse_bytes 162520
# HELP go_memstats_mspan_sys_bytes Number of bytes used for mspan structures obtained from system.
# TYPE go_memstats_mspan_sys_bytes gauge
go_memstats_mspan_sys_bytes 228480
# HELP go_memstats_next_gc_bytes Number of heap bytes when next garbage collection will take place.
# TYPE go_memstats_next_gc_bytes gauge
go_memstats_next_gc_bytes 1.3009584e+07
# HELP go_memstats_other_sys_bytes Number of bytes used for other system allocations.
# TYPE go_memstats_other_sys_bytes gauge
go_memstats_other_sys_bytes 1.193228e+06
# HELP go_memstats_stack_inuse_bytes Number of bytes in use by the stack allocator.
# TYPE go_memstats_stack_inuse_bytes gauge
go_memstats_stack_inuse_bytes 753664
# HELP go_memstats_stack_sys_bytes Number of bytes obtained from system for stack allocator.
# TYPE go_memstats_stack_sys_bytes gauge
go_memstats_stack_sys_bytes 753664
# HELP go_memstats_sys_bytes Number of bytes obtained from system.
# TYPE go_memstats_sys_bytes gauge
go_memstats_sys_bytes 3.3666312e+07
# HELP go_threads Number of OS threads created.
# TYPE go_threads gauge
go_threads 11
# HELP traefik_config_last_reload_failure Last config reload failure
# TYPE traefik_config_last_reload_failure gauge
traefik_config_last_reload_failure 0
# HELP traefik_config_last_reload_success Last config reload success
# TYPE traefik_config_last_reload_success gauge
traefik_config_last_reload_success 1.66463724e+09
# HELP traefik_config_reloads_failure_total Config failure reloads
# TYPE traefik_config_reloads_failure_total counter
traefik_config_reloads_failure_total 0
# HELP traefik_config_reloads_total Config reloads
# TYPE traefik_config_reloads_total counter
traefik_config_reloads_total 2
# HELP traefik_entrypoint_open_connections How many open connections exist on an entrypoint, partitioned by method and protocol.
# TYPE traefik_entrypoint_open_connections gauge
traefik_entrypoint_open_connections{entrypoint="metrics",method="GET",protocol="http"} 1
traefik_entrypoint_open_connections{entrypoint="web",method="GET",protocol="http"} 0
# HELP traefik_entrypoint_request_duration_seconds How long it took to process the request on an entrypoint, partitioned by status code, protocol, and method.
# TYPE traefik_entrypoint_request_duration_seconds histogram
traefik_entrypoint_request_duration_seconds_bucket{code="200",entrypoint="web",method="GET",protocol="http",le="0.1"} 1
traefik_entrypoint_request_duration_seconds_bucket{code="200",entrypoint="web",method="GET",protocol="http",le="0.3"} 1
traefik_entrypoint_request_duration_seconds_bucket{code="200",entrypoint="web",method="GET",protocol="http",le="1.2"} 1
traefik_entrypoint_request_duration_seconds_bucket{code="200",entrypoint="web",method="GET",protocol="http",le="5"} 1
traefik_entrypoint_request_duration_seconds_bucket{code="200",entrypoint="web",method="GET",protocol="http",le="+Inf"} 1
traefik_entrypoint_request_duration_seconds_sum{code="200",entrypoint="web",method="GET",protocol="http"} 0.019712622
traefik_entrypoint_request_duration_seconds_count{code="200",entrypoint="web",method="GET",protocol="http"} 1
# HELP traefik_entrypoint_requests_bytes_total The total size of requests in bytes handled by an entrypoint, partitioned by status code, protocol, and method.
# TYPE traefik_entrypoint_requests_bytes_total counter
traefik_entrypoint_requests_bytes_total{code="200",entrypoint="web",method="GET",protocol="http"} 0
# HELP traefik_entrypoint_requests_total How many HTTP requests processed on an entrypoint, partitioned by status code, protocol, and method.
# TYPE traefik_entrypoint_requests_total counter
traefik_entrypoint_requests_total{code="200",entrypoint="web",method="GET",protocol="http"} 1
# HELP traefik_entrypoint_responses_bytes_total The total size of responses in bytes handled by an entrypoint, partitioned by status code, protocol, and method.
# TYPE traefik_entrypoint_responses_bytes_total counter
traefik_entrypoint_responses_bytes_total{code="200",entrypoint="web",method="GET",protocol="http"} 198
# HELP traefik_service_open_connections How many open connections exist on a service, partitioned by method and protocol.
# TYPE traefik_service_open_connections gauge
traefik_service_open_connections{method="GET",protocol="http",service="test-http@file"} 0
# HELP traefik_service_request_duration_seconds How long it took to process the request on a service, partitioned by status code, protocol, and method.
# TYPE traefik_service_request_duration_seconds histogram
traefik_service_request_duration_seconds_bucket{code="0",method="GET",protocol="http",service="test-http@file",le="0.1"} 1
traefik_service_request_duration_seconds_bucket{code="0",method="GET",protocol="http",service="test-http@file",le="0.3"} 1
traefik_service_request_duration_seconds_bucket{code="0",method="GET",protocol="http",service="test-http@file",le="1.2"} 1
traefik_service_request_duration_seconds_bucket{code="0",method="GET",protocol="http",service="test-http@file",le="5"} 1
traefik_service_request_duration_seconds_bucket{code="0",method="GET",protocol="http",service="test-http@file",le="+Inf"} 1
traefik_service_request_duration_seconds_sum{code="0",method="GET",protocol="http",service="test-http@file"} 0.019640645
traefik_service_request_duration_seconds_count{code="0",method="GET",protocol="http",service="test-http@file"} 1
# HELP traefik_service_requests_bytes_total The total size of requests in bytes received by a service, partitioned by status code, protocol, and method.
# TYPE traefik_service_requests_bytes_total counter
traefik_service_requests_bytes_total{code="0",method="GET",protocol="http",service="test-http@file"} 0
# HELP traefik_service_requests_total How many HTTP requests processed on a service, partitioned by status code, protocol, and method.
# TYPE traefik_service_requests_total counter
traefik_service_requests_total{code="0",method="GET",protocol="http",service="test-http@file"} 1
# HELP traefik_service_responses_bytes_total The total size of responses in bytes returned by a service, partitioned by status code, protocol, and method.
# TYPE traefik_service_responses_bytes_total counter
traefik_service_responses_bytes_total{code="0",method="GET",protocol="http",service="test-http@file"} 0
```

### If applicable, please paste the log output in DEBUG level

_No response_


Discussion / hints:

Fix patch:
diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go
index f544773755..1250252517 100644
--- a/cmd/traefik/traefik.go
+++ b/cmd/traefik/traefik.go
@@ -31,7 +31,6 @@ import (
 	"github.com/traefik/traefik/v2/pkg/log"
 	"github.com/traefik/traefik/v2/pkg/metrics"
 	"github.com/traefik/traefik/v2/pkg/middlewares/accesslog"
-	"github.com/traefik/traefik/v2/pkg/middlewares/capture"
 	"github.com/traefik/traefik/v2/pkg/provider/acme"
 	"github.com/traefik/traefik/v2/pkg/provider/aggregator"
 	"github.com/traefik/traefik/v2/pkg/provider/hub"
@@ -260,9 +259,8 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
 
 	accessLog := setupAccessLog(staticConfiguration.AccessLog)
 	tracer := setupTracing(staticConfiguration.Tracing)
-	captureMiddleware := setupCapture(staticConfiguration)
 
-	chainBuilder := middleware.NewChainBuilder(metricsRegistry, accessLog, tracer, captureMiddleware)
+	chainBuilder := middleware.NewChainBuilder(metricsRegistry, accessLog, tracer)
 	routerFactory := server.NewRouterFactory(*staticConfiguration, managerFactory, tlsManager, chainBuilder, pluginBuilder, metricsRegistry)
 
 	// Watcher
@@ -565,13 +563,6 @@ func setupTracing(conf *static.Tracing) *tracing.Tracing {
 	return tracer
 }
 
-func setupCapture(staticConfiguration *static.Configuration) *capture.Handler {
-	if staticConfiguration.AccessLog == nil && staticConfiguration.Metrics == nil {
-		return nil
-	}
-	return &capture.Handler{}
-}
-
 func configureLogging(staticConfiguration *static.Configuration) {
 	// configure default log flags
 	stdlog.SetFlags(stdlog.Lshortfile | stdlog.LstdFlags)
diff --git a/integration/fixtures/simple_metrics_with_buffer_middleware.toml b/integration/fixtures/simple_metrics_with_buffer_middleware.toml
new file mode 100644
index 0000000000..2781c1981c
--- /dev/null
+++ b/integration/fixtures/simple_metrics_with_buffer_middleware.toml
@@ -0,0 +1,66 @@
+[global]
+  checkNewVersion = false
+  sendAnonymousUsage = false
+
+[log]
+  level = "DEBUG"
+
+[entryPoints]
+  [entryPoints.webA]
+    address = ":8001"
+  [entryPoints.webB]
+    address = ":8002"
+  [entryPoints.webC]
+    address = ":8003"
+
+[api]
+  insecure = true
+
+[metrics]
+  [metrics.prometheus]
+    buckets = "0.1,0.3,1.2,5.0"
+
+[providers]
+  [providers.file]
+    filename = "{{ .SelfFilename }}"
+
+## dynamic configuration ##
+
+[http.routers]
+
+  [http.routers.router-without]
+    entrypoints = ["webA"]
+    service = "service-without"
+    rule = "PathPrefix(`/without`)"
+
+  [http.routers.router-req]
+    entrypoints = ["webB"]
+    service = "service-req"
+    rule = "PathPrefix(`/with-req`)"
+    middlewares = ["buffer-req"]
+
+  [http.routers.router-resp]
+    entrypoints = ["webC"]
+    service = "service-resp"
+    rule = "PathPrefix(`/with-resp`)"
+    middlewares = ["buffer-resp"]
+
+[http.middlewares]
+  [http.middlewares.buffer-req.buffering]
+    maxRequestBodyBytes = 10
+
+  [http.middlewares.buffer-resp.buffering]
+    maxResponseBodyBytes = 10
+
+[http.services]
+  [http.services.service-without.loadBalancer]
+    [[http.services.service-without.loadBalancer.servers]]
+      url = "http://{{ .IP }}"
+
+  [http.services.service-req.loadBalancer]
+    [[http.services.service-req.loadBalancer.servers]]
+      url = "http://{{ .IP }}"
+
+  [http.services.service-resp.loadBalancer]
+    [[http.services.service-resp.loadBalancer.servers]]
+      url = "http://{{ .IP }}"
diff --git a/pkg/middlewares/accesslog/logger.go b/pkg/middlewares/accesslog/logger.go
index 8417285827..cf729c6def 100644
--- a/pkg/middlewares/accesslog/logger.go
+++ b/pkg/middlewares/accesslog/logger.go
@@ -227,6 +227,15 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http
 		core[ClientHost] = forwardedFor
 	}
 
+	ctx := req.Context()
+	capt, err := capture.FromContext(ctx)
+	if err != nil {
+		log.FromContext(log.With(ctx, log.Str(log.MiddlewareType, "AccessLogs"))).
+			WithError(err).
+			Errorf("Could not get Capture")
+		return
+	}
+
 	next.ServeHTTP(rw, reqWithDataTable)
 
 	if _, ok := core[ClientUsername]; !ok {
@@ -237,13 +246,6 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http
 		headers: rw.Header().Clone(),
 	}
 
-	ctx := req.Context()
-	capt, err := capture.FromContext(ctx)
-	if err != nil {
-		log.FromContext(log.With(ctx, log.Str(log.MiddlewareType, "AccessLogs"))).Errorf("Could not get Capture: %v", err)
-		return
-	}
-
 	logDataTable.DownstreamResponse.status = capt.StatusCode()
 	logDataTable.DownstreamResponse.size = capt.ResponseSize()
 	logDataTable.Request.size = capt.RequestSize()
diff --git a/pkg/middlewares/capture/capture.go b/pkg/middlewares/capture/capture.go
index 6ee0ed399b..845c2e983e 100644
--- a/pkg/middlewares/capture/capture.go
+++ b/pkg/middlewares/capture/capture.go
@@ -3,9 +3,8 @@
 // For another middleware to get those attributes of a request/response, this middleware
 // should be added before in the middleware chain.
 //
-//	handler, _ := NewHandler()
 //	chain := alice.New().
-//	     Append(WrapHandler(handler)).
+//	     Append(capture.Wrap).
 //	     Append(myOtherMiddleware).
 //	     then(...)
 //
@@ -33,7 +32,6 @@ import (
 	"net"
 	"net/http"
 
-	"github.com/containous/alice"
 	"github.com/traefik/traefik/v2/pkg/middlewares"
 )
 
@@ -41,62 +39,67 @@ type key string
 
 const capturedData key = "capturedData"
 
-// Handler will store each request data to its context.
-type Handler struct{}
-
-// WrapHandler wraps capture handler into an Alice Constructor.
-func WrapHandler(handler *Handler) alice.Constructor {
-	return func(next http.Handler) (http.Handler, error) {
-		return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
-			handler.ServeHTTP(rw, req, next)
-		}), nil
-	}
+// Wrap returns a new handler that inserts a Capture into the given handler.
+// It satisfies the alice.Constructor type.
+func Wrap(handler http.Handler) (http.Handler, error) {
+	c := Capture{}
+	return c.Reset(handler), nil
 }
 
-func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http.Handler) {
-	c := Capture{}
-	if req.Body != nil {
-		readCounter := &readCounter{source: req.Body}
-		c.rr = readCounter
-		req.Body = readCounter
+// FromContext returns the Capture value found in ctx, or an empty Capture otherwise.
+func FromContext(ctx context.Context) (Capture, error) {
+	c := ctx.Value(capturedData)
+	if c == nil {
+		return Capture{}, errors.New("value not found in context")
 	}
-	responseWriter := newResponseWriter(rw)
-	c.rw = responseWriter
-	ctx := context.WithValue(req.Context(), capturedData, &c)
-	next.ServeHTTP(responseWriter, req.WithContext(ctx))
+	capt, ok := c.(*Capture)
+	if !ok {
+		return Capture{}, errors.New("value stored in context is not a *Capture")
+	}
+	return *capt, nil
 }
 
 // Capture is the object populated by the capture middleware,
-// allowing to gather information about the request and response.
+// holding probes that allow to gather information about the request and response.
 type Capture struct {
 	rr *readCounter
 	rw responseWriter
 }
 
-// FromContext returns the Capture value found in ctx, or an empty Capture otherwise.
-func FromContext(ctx context.Context) (*Capture, error) {
-	c := ctx.Value(capturedData)
-	if c == nil {
-		return nil, errors.New("value not found")
-	}
-	capt, ok := c.(*Capture)
-	if !ok {
-		return nil, errors.New("value stored in Context is not a *Capture")
-	}
-	return capt, nil
+// NeedsReset returns whether the given http.ResponseWriter is the capture's probe.
+func (c *Capture) NeedsReset(rw http.ResponseWriter) bool {
+	return c.rw != rw
+}
+
+// Reset returns a new handler that renews the Capture's probes, and inserts
+// them when deferring to next.
+func (c *Capture) Reset(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		ctx := context.WithValue(req.Context(), capturedData, c)
+		newReq := req.WithContext(ctx)
+
+		if newReq.Body != nil {
+			readCounter := &readCounter{source: newReq.Body}
+			c.rr = readCounter
+			newReq.Body = readCounter
+		}
+		c.rw = newResponseWriter(rw)
+
+		next.ServeHTTP(c.rw, newReq)
+	})
 }
 
-func (c Capture) ResponseSize() int64 {
+func (c *Capture) ResponseSize() int64 {
 	return c.rw.Size()
 }
 
-func (c Capture) StatusCode() int {
+func (c *Capture) StatusCode() int {
 	return c.rw.Status()
 }
 
 // RequestSize returns the size of the request's body if it applies,
 // zero otherwise.
-func (c Capture) RequestSize() int64 {
+func (c *Capture) RequestSize() int64 {
 	if c.rr == nil {
 		return 0
 	}
diff --git a/pkg/middlewares/metrics/metrics.go b/pkg/middlewares/metrics/metrics.go
index 08d0185cc9..48ff699c7f 100644
--- a/pkg/middlewares/metrics/metrics.go
+++ b/pkg/middlewares/metrics/metrics.go
@@ -24,6 +24,7 @@ const (
 	protoWebsocket = "websocket"
 	typeName       = "Metrics"
 	nameEntrypoint = "metrics-entrypoint"
+	nameRouter     = "metrics-router"
 	nameService    = "metrics-service"
 )
 
@@ -56,7 +57,7 @@ func NewEntryPointMiddleware(ctx context.Context, next http.Handler, registry me
 
 // NewRouterMiddleware creates a new metrics middleware for a Router.
 func NewRouterMiddleware(ctx context.Context, next http.Handler, registry metrics.Registry, routerName string, serviceName string) http.Handler {
-	log.FromContext(middlewares.GetLoggerCtx(ctx, nameEntrypoint, typeName)).Debug("Creating middleware")
+	log.FromContext(middlewares.GetLoggerCtx(ctx, nameRouter, typeName)).Debug("Creating middleware")
 
 	return &metricsMiddleware{
 		next:                 next,
@@ -125,17 +126,25 @@ func (m *metricsMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Request)
 		m.reqsTLSCounter.With(tlsLabels...).Add(1)
 	}
 
-	start := time.Now()
-
-	m.next.ServeHTTP(rw, req)
-
 	ctx := req.Context()
+
 	capt, err := capture.FromContext(ctx)
 	if err != nil {
-		log.FromContext(middlewares.GetLoggerCtx(ctx, nameEntrypoint, typeName)).Errorf("Could not get Capture: %w", err)
+		for i := 0; i < len(m.baseLabels); i += 2 {
+			ctx = log.With(ctx, log.Str(m.baseLabels[i], m.baseLabels[i+1]))
+		}
+		log.FromContext(ctx).WithError(err).Errorf("Could not get Capture")
 		return
 	}
 
+	next := m.next
+	if capt.NeedsReset(rw) {
+		next = capt.Reset(m.next)
+	}
+
+	start := time.Now()
+	next.ServeHTTP(rw, req)
+
 	labels = append(labels, "code", strconv.Itoa(capt.StatusCode()))
 	m.reqDurationHistogram.With(labels...).ObserveFromStart(start)
 	m.reqsCounter.With(labels...).Add(1)
diff --git a/pkg/server/middleware/chainbuilder.go b/pkg/server/middleware/chainbuilder.go
index b4e1223a85..c5c8336060 100644
--- a/pkg/server/middleware/chainbuilder.go
+++ b/pkg/server/middleware/chainbuilder.go
@@ -18,16 +18,14 @@ type ChainBuilder struct {
 	metricsRegistry        metrics.Registry
 	accessLoggerMiddleware *accesslog.Handler
 	tracer                 *tracing.Tracing
-	captureMiddleware      *capture.Handler
 }
 
 // NewChainBuilder Creates a new ChainBuilder.
-func NewChainBuilder(metricsRegistry metrics.Registry, accessLoggerMiddleware *accesslog.Handler, tracer *tracing.Tracing, captureMiddleware *capture.Handler) *ChainBuilder {
+func NewChainBuilder(metricsRegistry metrics.Registry, accessLoggerMiddleware *accesslog.Handler, tracer *tracing.Tracing) *ChainBuilder {
 	return &ChainBuilder{
 		metricsRegistry:        metricsRegistry,
 		accessLoggerMiddleware: accessLoggerMiddleware,
 		tracer:                 tracer,
-		captureMiddleware:      captureMiddleware,
 	}
 }
 
@@ -35,8 +33,8 @@ func NewChainBuilder(metricsRegistry metrics.Registry, accessLoggerMiddleware *a
 func (c *ChainBuilder) Build(ctx context.Context, entryPointName string) alice.Chain {
 	chain := alice.New()
 
-	if c.captureMiddleware != nil {
-		chain = chain.Append(capture.WrapHandler(c.captureMiddleware))
+	if c.accessLoggerMiddleware != nil || c.metricsRegistry != nil && (c.metricsRegistry.IsEpEnabled() || c.metricsRegistry.IsRouterEnabled() || c.metricsRegistry.IsSvcEnabled()) {
+		chain = chain.Append(capture.Wrap)
 	}
 
 	if c.accessLoggerMiddleware != nil {


Test patch:
diff --git a/integration/simple_test.go b/integration/simple_test.go
index c939d8f0a9..28e5291fdc 100644
--- a/integration/simple_test.go
+++ b/integration/simple_test.go
@@ -308,7 +308,7 @@ func (s *SimpleSuite) TestMetricsPrometheusDefaultEntryPoint(c *check.C) {
 	c.Assert(err, checker.IsNil)
 	defer s.killCmd(cmd)
 
-	err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("PathPrefix"))
+	err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("PathPrefix(`/whoami`)"))
 	c.Assert(err, checker.IsNil)
 
 	err = try.GetRequest("http://127.0.0.1:8000/whoami", 1*time.Second, try.StatusCodeIs(http.StatusOK))
@@ -369,6 +369,84 @@ func (s *SimpleSuite) TestMetricsPrometheusTwoRoutersOneService(c *check.C) {
 	}
 }
 
+// TestMetricsWithBufferingMiddleware checks that the buffering middleware
+// (which introduces its own response writer in the chain), does not interfere with
+// the capture middleware on which the metrics mechanism relies.
+func (s *SimpleSuite) TestMetricsWithBufferingMiddleware(c *check.C) {
+	s.createComposeProject(c, "base")
+
+	s.composeUp(c)
+	defer s.composeDown(c)
+
+	server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusOK)
+		_, _ = w.Write([]byte("MORE THAN TEN BYTES IN RESPONSE"))
+	}))
+
+	server.Start()
+	defer server.Close()
+
+	file := s.adaptFile(c, "fixtures/simple_metrics_with_buffer_middleware.toml", struct{ IP string }{IP: strings.TrimPrefix(server.URL, "http://")})
+	defer os.Remove(file)
+
+	cmd, output := s.traefikCmd(withConfigFile(file))
+	defer output(c)
+
+	err := cmd.Start()
+	c.Assert(err, checker.IsNil)
+	defer s.killCmd(cmd)
+
+	err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("PathPrefix(`/without`)"))
+	c.Assert(err, checker.IsNil)
+
+	err = try.GetRequest("http://127.0.0.1:8001/without", 1*time.Second, try.StatusCodeIs(http.StatusOK))
+	c.Assert(err, checker.IsNil)
+
+	req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8002/with-req", strings.NewReader("MORE THAN TEN BYTES IN REQUEST"))
+	c.Assert(err, checker.IsNil)
+
+	// The request should fail because the body is too large.
+	err = try.Request(req, 1*time.Second, try.StatusCodeIs(http.StatusRequestEntityTooLarge))
+	c.Assert(err, checker.IsNil)
+
+	// The request should fail because the response exceeds the configured limit.
+	err = try.GetRequest("http://127.0.0.1:8003/with-resp", 1*time.Second, try.StatusCodeIs(http.StatusInternalServerError))
+	c.Assert(err, checker.IsNil)
+
+	request, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8080/metrics", nil)
+	c.Assert(err, checker.IsNil)
+
+	response, err := http.DefaultClient.Do(request)
+	c.Assert(err, checker.IsNil)
+	c.Assert(response.StatusCode, checker.Equals, http.StatusOK)
+
+	body, err := io.ReadAll(response.Body)
+	c.Assert(err, checker.IsNil)
+
+	// For allowed requests and responses, the entrypoint and service metrics have the same status code.
+	c.Assert(string(body), checker.Contains, "traefik_entrypoint_requests_total{code=\"200\",entrypoint=\"webA\",method=\"GET\",protocol=\"http\"} 1")
+	c.Assert(string(body), checker.Contains, "traefik_entrypoint_requests_bytes_total{code=\"200\",entrypoint=\"webA\",method=\"GET\",protocol=\"http\"} 0")
+	c.Assert(string(body), checker.Contains, "traefik_entrypoint_responses_bytes_total{code=\"200\",entrypoint=\"webA\",method=\"GET\",protocol=\"http\"} 31")
+
+	c.Assert(string(body), checker.Contains, "traefik_service_requests_total{code=\"200\",method=\"GET\",protocol=\"http\",service=\"service-without@file\"} 1")
+	c.Assert(string(body), checker.Contains, "traefik_service_requests_bytes_total{code=\"200\",method=\"GET\",protocol=\"http\",service=\"service-without@file\"} 0")
+	c.Assert(string(body), checker.Contains, "traefik_service_responses_bytes_total{code=\"200\",method=\"GET\",protocol=\"http\",service=\"service-without@file\"} 31")
+
+	// For forbidden requests, the entrypoints have metrics, the services don't.
+	c.Assert(string(body), checker.Contains, "traefik_entrypoint_requests_total{code=\"413\",entrypoint=\"webB\",method=\"GET\",protocol=\"http\"} 1")
+	c.Assert(string(body), checker.Contains, "traefik_entrypoint_requests_bytes_total{code=\"413\",entrypoint=\"webB\",method=\"GET\",protocol=\"http\"} 0")
+	c.Assert(string(body), checker.Contains, "traefik_entrypoint_responses_bytes_total{code=\"413\",entrypoint=\"webB\",method=\"GET\",protocol=\"http\"} 24")
+
+	// For disallowed responses, the entrypoint and service metrics don't have the same status code.
+	c.Assert(string(body), checker.Contains, "traefik_entrypoint_requests_bytes_total{code=\"500\",entrypoint=\"webC\",method=\"GET\",protocol=\"http\"} 0")
+	c.Assert(string(body), checker.Contains, "traefik_entrypoint_requests_total{code=\"500\",entrypoint=\"webC\",method=\"GET\",protocol=\"http\"} 1")
+	c.Assert(string(body), checker.Contains, "traefik_entrypoint_responses_bytes_total{code=\"500\",entrypoint=\"webC\",method=\"GET\",protocol=\"http\"} 21")
+
+	c.Assert(string(body), checker.Contains, "traefik_service_requests_bytes_total{code=\"200\",method=\"GET\",protocol=\"http\",service=\"service-resp@file\"} 0")
+	c.Assert(string(body), checker.Contains, "traefik_service_requests_total{code=\"200\",method=\"GET\",protocol=\"http\",service=\"service-resp@file\"} 1")
+	c.Assert(string(body), checker.Contains, "traefik_service_responses_bytes_total{code=\"200\",method=\"GET\",protocol=\"http\",service=\"service-resp@file\"} 31")
+}
+
 func (s *SimpleSuite) TestMultipleProviderSameBackendName(c *check.C) {
 	s.createComposeProject(c, "base")
 
diff --git a/pkg/middlewares/accesslog/logger_test.go b/pkg/middlewares/accesslog/logger_test.go
index b7b3b565e8..80bda1530d 100644
--- a/pkg/middlewares/accesslog/logger_test.go
+++ b/pkg/middlewares/accesslog/logger_test.go
@@ -57,7 +57,7 @@ func TestLogRotation(t *testing.T) {
 	})
 
 	chain := alice.New()
-	chain = chain.Append(capture.WrapHandler(&capture.Handler{}))
+	chain = chain.Append(capture.Wrap)
 	chain = chain.Append(WrapHandler(logHandler))
 	handler, err := chain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
 		rw.WriteHeader(http.StatusOK)
@@ -210,7 +210,7 @@ func TestLoggerHeaderFields(t *testing.T) {
 			}
 
 			chain := alice.New()
-			chain = chain.Append(capture.WrapHandler(&capture.Handler{}))
+			chain = chain.Append(capture.Wrap)
 			chain = chain.Append(WrapHandler(logger))
 			handler, err := chain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
 				rw.WriteHeader(http.StatusOK)
@@ -784,7 +784,7 @@ func doLoggingTLSOpt(t *testing.T, config *types.AccessLog, enableTLS bool) {
 	}
 
 	chain := alice.New()
-	chain = chain.Append(capture.WrapHandler(&capture.Handler{}))
+	chain = chain.Append(capture.Wrap)
 	chain = chain.Append(WrapHandler(logger))
 	handler, err := chain.Then(http.HandlerFunc(logWriterTestHandlerFunc))
 	require.NoError(t, err)
diff --git a/pkg/middlewares/capture/capture_test.go b/pkg/middlewares/capture/capture_test.go
index f6718ad6e0..9d8f91fcf2 100644
--- a/pkg/middlewares/capture/capture_test.go
+++ b/pkg/middlewares/capture/capture_test.go
@@ -38,9 +38,8 @@ func TestCapture(t *testing.T) {
 		assert.Equal(t, "bar", string(all))
 	})
 
-	wrapped := WrapHandler(&Handler{})
 	chain := alice.New()
-	chain = chain.Append(wrapped)
+	chain = chain.Append(Wrap)
 	chain = chain.Append(wrapMiddleware)
 	handlers, err := chain.Then(handler)
 	require.NoError(t, err)
@@ -142,8 +141,7 @@ func BenchmarkCapture(b *testing.B) {
 
 			chain := alice.New()
 			if test.capture || test.body {
-				captureWrapped := WrapHandler(&Handler{})
-				chain = chain.Append(captureWrapped)
+				chain = chain.Append(Wrap)
 			}
 			handlers, err := chain.Then(next)
 			require.NoError(b, err)
diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go
index 867da0a9b6..26e8a68a34 100644
--- a/pkg/server/router/router_test.go
+++ b/pkg/server/router/router_test.go
@@ -316,7 +316,7 @@ func TestRouterManager_Get(t *testing.T) {
 			roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
 			serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager)
 			middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
-			chainBuilder := middleware.NewChainBuilder(nil, nil, nil, nil)
+			chainBuilder := middleware.NewChainBuilder(nil, nil, nil)
 
 			routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry())
 
@@ -422,7 +422,7 @@ func TestAccessLog(t *testing.T) {
 			roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
 			serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager)
 			middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
-			chainBuilder := middleware.NewChainBuilder(nil, nil, nil, nil)
+			chainBuilder := middleware.NewChainBuilder(nil, nil, nil)
 
 			routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry())
 
@@ -439,7 +439,7 @@ func TestAccessLog(t *testing.T) {
 			reqHost := requestdecorator.New(nil)
 
 			chain := alice.New()
-			chain = chain.Append(capture.WrapHandler(&capture.Handler{}))
+			chain = chain.Append(capture.Wrap)
 			chain = chain.Append(accesslog.WrapHandler(accesslogger))
 			handler, err := chain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
 				reqHost.ServeHTTP(w, req, handlers["web"].ServeHTTP)
@@ -717,7 +717,7 @@ func TestRuntimeConfiguration(t *testing.T) {
 			roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
 			serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager)
 			middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
-			chainBuilder := middleware.NewChainBuilder(nil, nil, nil, nil)
+			chainBuilder := middleware.NewChainBuilder(nil, nil, nil)
 
 			routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry())
 
@@ -792,7 +792,7 @@ func TestProviderOnMiddlewares(t *testing.T) {
 	roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
 	serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager)
 	middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
-	chainBuilder := middleware.NewChainBuilder(nil, nil, nil, nil)
+	chainBuilder := middleware.NewChainBuilder(nil, nil, nil)
 
 	routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry())
 
@@ -860,7 +860,7 @@ func BenchmarkRouterServe(b *testing.B) {
 
 	serviceManager := service.NewManager(rtConf.Services, nil, nil, staticRoundTripperGetter{res})
 	middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
-	chainBuilder := middleware.NewChainBuilder(nil, nil, nil, nil)
+	chainBuilder := middleware.NewChainBuilder(nil, nil, nil)
 
 	routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry())
 
diff --git a/pkg/server/routerfactory_test.go b/pkg/server/routerfactory_test.go
index 0c6697e38d..fdf4efc696 100644
--- a/pkg/server/routerfactory_test.go
+++ b/pkg/server/routerfactory_test.go
@@ -53,7 +53,7 @@ func TestReuseService(t *testing.T) {
 	managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry(), roundTripperManager, nil)
 	tlsManager := tls.NewManager()
 
-	factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(nil, nil, nil, nil), nil, metrics.NewVoidRegistry())
+	factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(nil, nil, nil), nil, metrics.NewVoidRegistry())
 
 	entryPointsHandlers, _ := factory.CreateRouters(runtime.NewConfig(dynamic.Configuration{HTTP: dynamicConfigs}))
 
@@ -189,7 +189,7 @@ func TestServerResponseEmptyBackend(t *testing.T) {
 			managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry(), roundTripperManager, nil)
 			tlsManager := tls.NewManager()
 
-			factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(nil, nil, nil, nil), nil, metrics.NewVoidRegistry())
+			factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(nil, nil, nil), nil, metrics.NewVoidRegistry())
 
 			entryPointsHandlers, _ := factory.CreateRouters(runtime.NewConfig(dynamic.Configuration{HTTP: test.config(testServer.URL)}))
 
@@ -232,7 +232,7 @@ func TestInternalServices(t *testing.T) {
 
 	voidRegistry := metrics.NewVoidRegistry()
 
-	factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(voidRegistry, nil, nil, nil), nil, voidRegistry)
+	factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(voidRegistry, nil, nil), nil, voidRegistry)
 
 	entryPointsHandlers, _ := factory.CreateRouters(runtime.NewConfig(dynamic.Configuration{HTTP: dynamicConfigs}))