Repository: open-telemetry/opentelemetry-go-contrib (go)

Issue:
Data race in v0.53.0 `instrumentation/net/http/otelhttp.(*bodyWrapper).Read()`
### Description

Seen during local runs of Kubernetes integration tests.

```
==================
WARNING: DATA RACE
Write at 0x00c012172560 by goroutine 448906:
  go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp.(*bodyWrapper).Read()
      ROOT/src/github.com/kubernetes/kubernetes/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/wrap.go:31 +0x8c
  io.(*LimitedReader).Read()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/io/io.go:479 +0xbc
  io.ReadAll()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/io/io.go:712 +0x84
  io/ioutil.ReadAll()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/io/ioutil/ioutil.go:27 +0x1fc
  k8s.io/apiserver/pkg/endpoints/handlers.limitedReadBody()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go:382 +0x1ec
  k8s.io/apiserver/pkg/endpoints/handlers.limitedReadBodyWithRecordMetric()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go:393 +0x48
  k8s.io/apiextensions-apiserver/pkg/apiserver.(*crdHandler).serveResource.CreateResource.createHandler.func9()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go:93 +0x4c0
  k8s.io/apiextensions-apiserver/pkg/apiserver.(*crdHandler).ServeHTTP.InstrumentHandlerFunc.func1()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/metrics/metrics.go:617 +0x184
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiextensions-apiserver/pkg/apiserver.(*crdHandler).ServeHTTP.WithWaitGroup.withWaitGroup.func2()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/filters/waitgroup.go:86 +0x170
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiextensions-apiserver/pkg/apiserver.(*crdHandler).ServeHTTP()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go:355 +0x1890
  k8s.io/apiserver/pkg/server/mux.(*pathHandler).ServeHTTP()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/mux/pathrecorder.go:251 +0x4f0
  k8s.io/apiserver/pkg/server/mux.(*PathRecorderMux).ServeHTTP()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/mux/pathrecorder.go:237 +0x60
  k8s.io/apiserver/pkg/server.director.ServeHTTP()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/handler.go:154 +0x844
  k8s.io/apiserver/pkg/server.(*director).ServeHTTP()
      <autogenerated>:1 +0x74
  k8s.io/apiserver/pkg/server/mux.(*pathHandler).ServeHTTP()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/mux/pathrecorder.go:257 +0x68c
  k8s.io/apiserver/pkg/server/mux.(*PathRecorderMux).ServeHTTP()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/mux/pathrecorder.go:237 +0x60
  k8s.io/apiserver/pkg/server.director.ServeHTTP()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/handler.go:154 +0x844
  k8s.io/apiserver/pkg/server.(*director).ServeHTTP()
      <autogenerated>:1 +0x74
  k8s.io/kube-aggregator/pkg/apiserver.(*proxyHandler).ServeHTTP()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_proxy.go:118 +0x224
  k8s.io/apiserver/pkg/server/mux.(*pathHandler).ServeHTTP()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/mux/pathrecorder.go:251 +0x4f0
  k8s.io/apiserver/pkg/server/mux.(*PathRecorderMux).ServeHTTP()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/mux/pathrecorder.go:237 +0x60
  k8s.io/apiserver/pkg/server.director.ServeHTTP()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/handler.go:154 +0x844
  k8s.io/apiserver/pkg/server.(*director).ServeHTTP()
      <autogenerated>:1 +0x74
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.TrackCompleted.trackCompleted.func22.deferwrap1()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filterlatency/filterlatency.go:103 +0x6c
  runtime.deferreturn()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/runtime/panic.go:602 +0x5c
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/endpoints/filters.withAuthorization.func1()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authorization.go:78 +0x5a8
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/endpoints/filterlatency.trackStarted.func1()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filterlatency/filterlatency.go:84 +0x1c0
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.TrackCompleted.trackCompleted.func23.deferwrap1()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filterlatency/filterlatency.go:103 +0x6c
  runtime.deferreturn()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/runtime/panic.go:602 +0x5c
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server/filters.(*priorityAndFairnessHandler).Handle.func9()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/filters/priority-and-fairness.go:292 +0x108
  k8s.io/apiserver/pkg/util/flowcontrol.(*configController).Handle.func2()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/util/flowcontrol/apf_filter.go:192 +0x2ac
  k8s.io/apiserver/pkg/util/flowcontrol/fairqueuing/queueset.(*request).Finish.func1()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/util/flowcontrol/fairqueuing/queueset/queueset.go:391 +0x88
  k8s.io/apiserver/pkg/util/flowcontrol/fairqueuing/queueset.(*request).Finish()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/util/flowcontrol/fairqueuing/queueset/queueset.go:392 +0x44
  k8s.io/apiserver/pkg/util/flowcontrol.(*configController).Handle()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/util/flowcontrol/apf_filter.go:179 +0x9f8
  k8s.io/apiserver/pkg/server/filters.(*priorityAndFairnessHandler).Handle.func10()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/filters/priority-and-fairness.go:298 +0xe0
  k8s.io/apiserver/pkg/server/filters.(*priorityAndFairnessHandler).Handle()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/filters/priority-and-fairness.go:299 +0xff4
  k8s.io/apiserver/pkg/server/filters.(*priorityAndFairnessHandler).Handle-fm()
      <autogenerated>:1 +0x4c
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/endpoints/filterlatency.trackStarted.func1()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filterlatency/filterlatency.go:84 +0x1c0
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.TrackCompleted.trackCompleted.func24.deferwrap1()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filterlatency/filterlatency.go:103 +0x6c
  runtime.deferreturn()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/runtime/panic.go:602 +0x5c
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithImpersonation.func4()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filters/impersonation.go:50 +0x178
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/endpoints/filterlatency.trackStarted.func1()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filterlatency/filterlatency.go:84 +0x1c0
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.TrackCompleted.trackCompleted.func25.deferwrap1()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filterlatency/filterlatency.go:103 +0x6c
  runtime.deferreturn()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/runtime/panic.go:602 +0x5c
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithAudit.func6()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit.go:55 +0x3d4
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/endpoints/filterlatency.trackStarted.func1()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filterlatency/filterlatency.go:84 +0x1c0
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.TrackCompleted.trackCompleted.func27.deferwrap1()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filterlatency/filterlatency.go:103 +0x6c
  runtime.deferreturn()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/runtime/panic.go:602 +0x5c
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/endpoints/filters.withAuthentication.func1()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authentication.go:120 +0x894
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/endpoints/filterlatency.trackStarted.func1()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filterlatency/filterlatency.go:94 +0x3a8
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithWarningRecorder.func11()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filters/warning.go:35 +0x10c
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server/filters.(*timeoutHandler).ServeHTTP.func1()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/filters/timeout.go:115 +0xb8

Previous read at 0x00c012172560 by goroutine 448900:
  go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp.(*middleware).serveHTTP()
      ROOT/src/github.com/kubernetes/kubernetes/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/handler.go:224 +0x14b8
  go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp.NewMiddleware.func1.1()
      ROOT/src/github.com/kubernetes/kubernetes/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/handler.go:74 +0x5c
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithLatencyTrackers.func16()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filters/webhook_duration.go:56 +0x194
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithRoutine.func17()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/routine/routine.go:65 +0x94
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithRequestInfo.func18()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filters/requestinfo.go:39 +0x138
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithRequestReceivedTimestamp.withRequestReceivedTimestampWithClock.func32()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filters/request_received_time.go:38 +0xb0
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithMuxAndDiscoveryComplete.func19()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filters/mux_discovery_complete.go:52 +0xb8
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithPanicRecovery.withPanicRecovery.func33()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/filters/wrap.go:73 +0x17c
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithAuditInit.withAuditInit.func34()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit_init.go:63 +0x124
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.(*APIServerHandler).ServeHTTP()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/handler.go:189 +0x5c
  net/http.serverHandler.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:3137 +0x2a8
  net/http.initALPNRequest.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:3745 +0x268
  net/http.(*initALPNRequest).ServeHTTP()
      <autogenerated>:1 +0x74
  net/http.Handler.ServeHTTP-fm()
      <autogenerated>:1 +0x60
  golang.org/x/net/http2.(*serverConn).runHandler()
      ROOT/src/github.com/kubernetes/kubernetes/vendor/golang.org/x/net/http2/server.go:2414 +0x194
  golang.org/x/net/http2.(*serverConn).scheduleHandler.gowrap1()
      ROOT/src/github.com/kubernetes/kubernetes/vendor/golang.org/x/net/http2/server.go:2348 +0x58

Goroutine 448906 (running) created at:
  k8s.io/apiserver/pkg/server/filters.(*timeoutHandler).ServeHTTP()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/filters/timeout.go:101 +0x250
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithRequestDeadline.withRequestDeadline.func28()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filters/request_deadline.go:100 +0x1b0
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithWaitGroup.withWaitGroup.func29()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/filters/waitgroup.go:86 +0x170
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithCacheControl.func14()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filters/cachecontrol.go:31 +0xbc
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithHTTPLogging.WithLogging.withLogging.func35()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/httplog/httplog.go:139 +0x1e8
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/endpoints/filters.WithTracing.func2()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filters/traces.go:51 +0x1d0
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp.(*middleware).serveHTTP()
      ROOT/src/github.com/kubernetes/kubernetes/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/handler.go:218 +0x141c
  go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp.NewMiddleware.func1.1()
      ROOT/src/github.com/kubernetes/kubernetes/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/handler.go:74 +0x5c
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithLatencyTrackers.func16()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filters/webhook_duration.go:56 +0x194
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithRoutine.func17()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/routine/routine.go:65 +0x94
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithRequestInfo.func18()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filters/requestinfo.go:39 +0x138
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithRequestReceivedTimestamp.withRequestReceivedTimestampWithClock.func32()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filters/request_received_time.go:38 +0xb0
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithMuxAndDiscoveryComplete.func19()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filters/mux_discovery_complete.go:52 +0xb8
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithPanicRecovery.withPanicRecovery.func33()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/filters/wrap.go:73 +0x17c
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.DefaultBuildHandlerChain.WithAuditInit.withAuditInit.func34()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit_init.go:63 +0x124
  net/http.HandlerFunc.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:2166 +0x48
  k8s.io/apiserver/pkg/server.(*APIServerHandler).ServeHTTP()
      ROOT/src/github.com/kubernetes/kubernetes/staging/src/k8s.io/apiserver/pkg/server/handler.go:189 +0x5c
  net/http.serverHandler.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:3137 +0x2a8
  net/http.initALPNRequest.ServeHTTP()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:3745 +0x268
  net/http.(*initALPNRequest).ServeHTTP()
      <autogenerated>:1 +0x74
  net/http.Handler.ServeHTTP-fm()
      <autogenerated>:1 +0x60
  golang.org/x/net/http2.(*serverConn).runHandler()
      ROOT/src/github.com/kubernetes/kubernetes/vendor/golang.org/x/net/http2/server.go:2414 +0x194
  golang.org/x/net/http2.(*serverConn).scheduleHandler.gowrap1()
      ROOT/src/github.com/kubernetes/kubernetes/vendor/golang.org/x/net/http2/server.go:2348 +0x58

Goroutine 448900 (finished) created at:
  golang.org/x/net/http2.(*serverConn).scheduleHandler()
      ROOT/src/github.com/kubernetes/kubernetes/vendor/golang.org/x/net/http2/server.go:2348 +0x1f8
  golang.org/x/net/http2.(*serverConn).processHeaders()
      ROOT/src/github.com/kubernetes/kubernetes/vendor/golang.org/x/net/http2/server.go:2070 +0x980
  golang.org/x/net/http2.(*serverConn).processFrame()
      ROOT/src/github.com/kubernetes/kubernetes/vendor/golang.org/x/net/http2/server.go:1571 +0x200
  golang.org/x/net/http2.(*serverConn).processFrameFromReader()
      ROOT/src/github.com/kubernetes/kubernetes/vendor/golang.org/x/net/http2/server.go:1509 +0x2a0
  golang.org/x/net/http2.(*serverConn).serve()
      ROOT/src/github.com/kubernetes/kubernetes/vendor/golang.org/x/net/http2/server.go:998 +0x1130
  golang.org/x/net/http2.(*Server).serveConn()
      ROOT/src/github.com/kubernetes/kubernetes/vendor/golang.org/x/net/http2/server.go:572 +0x13b0
  golang.org/x/net/http2.(*Server).ServeConn()
      ROOT/src/github.com/kubernetes/kubernetes/vendor/golang.org/x/net/http2/server.go:436 +0x154
  golang.org/x/net/http2.ConfigureServer.func1()
      ROOT/src/github.com/kubernetes/kubernetes/vendor/golang.org/x/net/http2/server.go:355 +0xc0
  net/http.(*conn).serve()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:1947 +0xa60
  net/http.(*Server).Serve.gowrap3()
      HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.3.darwin-arm64/src/net/http/server.go:3285 +0x4c
==================
```

### Environment

- OS: macOS
- Architecture: arm64
- Go Version: 1.22.3
- `go.opentelemetry.io/contrib` version: v0.53.0

### Steps To Reproduce

1. Happens rarely during intergration test runs of Kube

### Expected behavior

No data race.  The following line likely needs synchronization:

https://github.com/open-telemetry/opentelemetry-go-contrib/blob/e11fd0733809e578cc9cbf9fea4a25d85b5bff77/instrumentation/net/http/otelhttp/wrap.go#L31

Pull request:
Move otelhttp wrappers into internal package
This moves the otelhttp wrappers into the internal package, so we cannot rely on private values, and are forced to think about race conditions when using that data.

Closes #5914.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1c5d1a378..41521bc3d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 
 - The deprecated `go.opentelemetry.io/contrib/processors/baggagecopy` package is removed. (#5853)
 
+### Fixed
+
+- Race condition when reading the HTTP body and writing the response in `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp`. (#5916)
+
 <!-- Released section -->
 <!-- Don't change this section unless doing release -->
 
diff --git a/instrumentation/net/http/otelhttp/handler.go b/instrumentation/net/http/otelhttp/handler.go
index c49069d1f..5169ebf02 100644
--- a/instrumentation/net/http/otelhttp/handler.go
+++ b/instrumentation/net/http/otelhttp/handler.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/felixge/httpsnoop"
 
+	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request"
 	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv"
 	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil"
 	"go.opentelemetry.io/otel"
@@ -166,14 +167,12 @@ func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http
 		}
 	}
 
-	var bw bodyWrapper
 	// if request body is nil or NoBody, we don't want to mutate the body as it
 	// will affect the identity of it in an unforeseeable way because we assert
 	// ReadCloser fulfills a certain interface and it is indeed nil or NoBody.
+	bw := request.NewBodyWrapper(r.Body, readRecordFunc)
 	if r.Body != nil && r.Body != http.NoBody {
-		bw.ReadCloser = r.Body
-		bw.record = readRecordFunc
-		r.Body = &bw
+		r.Body = bw
 	}
 
 	writeRecordFunc := func(int64) {}
@@ -183,13 +182,7 @@ func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http
 		}
 	}
 
-	rww := &respWriterWrapper{
-		ResponseWriter: w,
-		record:         writeRecordFunc,
-		ctx:            ctx,
-		props:          h.propagators,
-		statusCode:     http.StatusOK, // default status code in case the Handler doesn't write anything
-	}
+	rww := request.NewRespWriterWrapper(w, writeRecordFunc)
 
 	// Wrap w to use our ResponseWriter methods while also exposing
 	// other interfaces that w may implement (http.CloseNotifier,
@@ -217,24 +210,26 @@ func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http
 
 	next.ServeHTTP(w, r.WithContext(ctx))
 
-	span.SetStatus(semconv.ServerStatus(rww.statusCode))
+	statusCode := rww.StatusCode()
+	bytesWritten := rww.BytesWritten()
+	span.SetStatus(semconv.ServerStatus(statusCode))
 	span.SetAttributes(h.traceSemconv.ResponseTraceAttrs(semconv.ResponseTelemetry{
-		StatusCode: rww.statusCode,
-		ReadBytes:  bw.read.Load(),
-		ReadError:  bw.err,
-		WriteBytes: rww.written,
-		WriteError: rww.err,
+		StatusCode: statusCode,
+		ReadBytes:  bw.BytesRead(),
+		ReadError:  bw.Error(),
+		WriteBytes: bytesWritten,
+		WriteError: rww.Error(),
 	})...)
 
 	// Add metrics
 	attributes := append(labeler.Get(), semconvutil.HTTPServerRequestMetrics(h.server, r)...)
-	if rww.statusCode > 0 {
-		attributes = append(attributes, semconv.HTTPStatusCode(rww.statusCode))
+	if statusCode > 0 {
+		attributes = append(attributes, semconv.HTTPStatusCode(statusCode))
 	}
 	o := metric.WithAttributeSet(attribute.NewSet(attributes...))
 
-	h.requestBytesCounter.Add(ctx, bw.read.Load(), o)
-	h.responseBytesCounter.Add(ctx, rww.written, o)
+	h.requestBytesCounter.Add(ctx, bw.BytesRead(), o)
+	h.responseBytesCounter.Add(ctx, bytesWritten, o)
 
 	// Use floating point division here for higher precision (instead of Millisecond method).
 	elapsedTime := float64(time.Since(requestStartTime)) / float64(time.Millisecond)
diff --git a/instrumentation/net/http/otelhttp/internal/request/body_wrapper.go b/instrumentation/net/http/otelhttp/internal/request/body_wrapper.go
new file mode 100644
index 000000000..a945f5566
--- /dev/null
+++ b/instrumentation/net/http/otelhttp/internal/request/body_wrapper.go
@@ -0,0 +1,75 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package request // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request"
+
+import (
+	"io"
+	"sync"
+)
+
+var _ io.ReadCloser = &BodyWrapper{}
+
+// BodyWrapper wraps a http.Request.Body (an io.ReadCloser) to track the number
+// of bytes read and the last error.
+type BodyWrapper struct {
+	io.ReadCloser
+	OnRead func(n int64) // must not be nil
+
+	mu   sync.Mutex
+	read int64
+	err  error
+}
+
+// NewBodyWrapper creates a new BodyWrapper.
+//
+// The onRead attribute is a callback that will be called every time the data
+// is read, with the number of bytes being read.
+func NewBodyWrapper(body io.ReadCloser, onRead func(int64)) *BodyWrapper {
+	return &BodyWrapper{
+		ReadCloser: body,
+		OnRead:     onRead,
+	}
+}
+
+// Read reads the data from the io.ReadCloser, and stores the number of bytes
+// read and the error.
+func (w *BodyWrapper) Read(b []byte) (int, error) {
+	n, err := w.ReadCloser.Read(b)
+	n1 := int64(n)
+
+	w.updateReadData(n1, err)
+	w.OnRead(n1)
+	return n, err
+}
+
+func (w *BodyWrapper) updateReadData(n int64, err error) {
+	w.mu.Lock()
+	defer w.mu.Unlock()
+
+	w.read += n
+	if err != nil {
+		w.err = err
+	}
+}
+
+// Closes closes the io.ReadCloser.
+func (w *BodyWrapper) Close() error {
+	return w.ReadCloser.Close()
+}
+
+// BytesRead returns the number of bytes read up to this point.
+func (w *BodyWrapper) BytesRead() int64 {
+	w.mu.Lock()
+	defer w.mu.Unlock()
+
+	return w.read
+}
+
+// Error returns the last error.
+func (w *BodyWrapper) Error() error {
+	w.mu.Lock()
+	defer w.mu.Unlock()
+
+	return w.err
+}
diff --git a/instrumentation/net/http/otelhttp/internal/request/resp_writer_wrapper.go b/instrumentation/net/http/otelhttp/internal/request/resp_writer_wrapper.go
new file mode 100644
index 000000000..aea171fb2
--- /dev/null
+++ b/instrumentation/net/http/otelhttp/internal/request/resp_writer_wrapper.go
@@ -0,0 +1,112 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package request // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request"
+
+import (
+	"net/http"
+	"sync"
+)
+
+var _ http.ResponseWriter = &RespWriterWrapper{}
+
+// RespWriterWrapper wraps a http.ResponseWriter in order to track the number of
+// bytes written, the last error, and to catch the first written statusCode.
+// TODO: The wrapped http.ResponseWriter doesn't implement any of the optional
+// types (http.Hijacker, http.Pusher, http.CloseNotifier, etc)
+// that may be useful when using it in real life situations.
+type RespWriterWrapper struct {
+	http.ResponseWriter
+	OnWrite func(n int64) // must not be nil
+
+	mu          sync.RWMutex
+	written     int64
+	statusCode  int
+	err         error
+	wroteHeader bool
+}
+
+// NewRespWriterWrapper creates a new RespWriterWrapper.
+//
+// The onWrite attribute is a callback that will be called every time the data
+// is written, with the number of bytes that were written.
+func NewRespWriterWrapper(w http.ResponseWriter, onWrite func(int64)) *RespWriterWrapper {
+	return &RespWriterWrapper{
+		ResponseWriter: w,
+		OnWrite:        onWrite,
+		statusCode:     http.StatusOK, // default status code in case the Handler doesn't write anything
+	}
+}
+
+// Write writes the bytes array into the [ResponseWriter], and tracks the
+// number of bytes written and last error.
+func (w *RespWriterWrapper) Write(p []byte) (int, error) {
+	w.mu.Lock()
+	defer w.mu.Unlock()
+
+	w.writeHeader(http.StatusOK)
+
+	n, err := w.ResponseWriter.Write(p)
+	n1 := int64(n)
+	w.OnWrite(n1)
+	w.written += n1
+	w.err = err
+	return n, err
+}
+
+// WriteHeader persists initial statusCode for span attribution.
+// All calls to WriteHeader will be propagated to the underlying ResponseWriter
+// and will persist the statusCode from the first call.
+// Blocking consecutive calls to WriteHeader alters expected behavior and will
+// remove warning logs from net/http where developers will notice incorrect handler implementations.
+func (w *RespWriterWrapper) WriteHeader(statusCode int) {
+	w.mu.Lock()
+	defer w.mu.Unlock()
+
+	w.writeHeader(statusCode)
+}
+
+// writeHeader persists the status code for span attribution, and propagates
+// the call to the underlying ResponseWriter.
+// It does not acquire a lock, and therefore assumes that is being handled by a
+// parent method.
+func (w *RespWriterWrapper) writeHeader(statusCode int) {
+	if !w.wroteHeader {
+		w.wroteHeader = true
+		w.statusCode = statusCode
+	}
+	w.ResponseWriter.WriteHeader(statusCode)
+}
+
+// Flush implements [http.Flusher].
+func (w *RespWriterWrapper) Flush() {
+	w.WriteHeader(http.StatusOK)
+
+	if f, ok := w.ResponseWriter.(http.Flusher); ok {
+		f.Flush()
+	}
+}
+
+// BytesWritten returns the number of bytes written.
+func (w *RespWriterWrapper) BytesWritten() int64 {
+	w.mu.RLock()
+	defer w.mu.RUnlock()
+
+	return w.written
+}
+
+// BytesWritten returns the HTTP status code that was sent.
+func (w *RespWriterWrapper) StatusCode() int {
+	w.mu.RLock()
+	defer w.mu.RUnlock()
+
+	return w.statusCode
+}
+
+// Error returns the last error.
+func (w *RespWriterWrapper) Error() error {
+	w.mu.RLock()
+	defer w.mu.RUnlock()
+
+	return w.err
+}
diff --git a/instrumentation/net/http/otelhttp/transport.go b/instrumentation/net/http/otelhttp/transport.go
index be17d4c75..fc4dd98f3 100644
--- a/instrumentation/net/http/otelhttp/transport.go
+++ b/instrumentation/net/http/otelhttp/transport.go
@@ -11,6 +11,7 @@ import (
 	"sync/atomic"
 	"time"
 
+	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request"
 	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv"
 	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil"
 	"go.opentelemetry.io/otel"
@@ -146,16 +147,12 @@ func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) {
 
 	r = r.Clone(ctx) // According to RoundTripper spec, we shouldn't modify the origin request.
 
-	// use a body wrapper to determine the request size
-	var bw bodyWrapper
 	// if request body is nil or NoBody, we don't want to mutate the body as it
 	// will affect the identity of it in an unforeseeable way because we assert
 	// ReadCloser fulfills a certain interface and it is indeed nil or NoBody.
+	bw := request.NewBodyWrapper(r.Body, func(int64) {})
 	if r.Body != nil && r.Body != http.NoBody {
-		bw.ReadCloser = r.Body
-		// noop to prevent nil panic. not using this record fun yet.
-		bw.record = func(int64) {}
-		r.Body = &bw
+		r.Body = bw
 	}
 
 	span.SetAttributes(t.semconv.RequestTraceAttrs(r)...)
@@ -184,7 +181,7 @@ func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) {
 	}
 	o := metric.WithAttributeSet(attribute.NewSet(metricAttrs...))
 
-	t.requestBytesCounter.Add(ctx, bw.read.Load(), o)
+	t.requestBytesCounter.Add(ctx, bw.BytesRead(), o)
 	// For handling response bytes we leverage a callback when the client reads the http response
 	readRecordFunc := func(n int64) {
 		t.responseBytesCounter.Add(ctx, n, o)
diff --git a/instrumentation/net/http/otelhttp/wrap.go b/instrumentation/net/http/otelhttp/wrap.go
deleted file mode 100644
index 948f8406c..000000000
--- a/instrumentation/net/http/otelhttp/wrap.go
+++ /dev/null
@@ -1,99 +0,0 @@
-// Copyright The OpenTelemetry Authors
-// SPDX-License-Identifier: Apache-2.0
-
-package otelhttp // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
-
-import (
-	"context"
-	"io"
-	"net/http"
-	"sync/atomic"
-
-	"go.opentelemetry.io/otel/propagation"
-)
-
-var _ io.ReadCloser = &bodyWrapper{}
-
-// bodyWrapper wraps a http.Request.Body (an io.ReadCloser) to track the number
-// of bytes read and the last error.
-type bodyWrapper struct {
-	io.ReadCloser
-	record func(n int64) // must not be nil
-
-	read atomic.Int64
-	err  error
-}
-
-func (w *bodyWrapper) Read(b []byte) (int, error) {
-	n, err := w.ReadCloser.Read(b)
-	n1 := int64(n)
-	w.read.Add(n1)
-	w.err = err
-	w.record(n1)
-	return n, err
-}
-
-func (w *bodyWrapper) Close() error {
-	return w.ReadCloser.Close()
-}
-
-var _ http.ResponseWriter = &respWriterWrapper{}
-
-// respWriterWrapper wraps a http.ResponseWriter in order to track the number of
-// bytes written, the last error, and to catch the first written statusCode.
-// TODO: The wrapped http.ResponseWriter doesn't implement any of the optional
-// types (http.Hijacker, http.Pusher, http.CloseNotifier, http.Flusher, etc)
-// that may be useful when using it in real life situations.
-type respWriterWrapper struct {
-	http.ResponseWriter
-	record func(n int64) // must not be nil
-
-	// used to inject the header
-	ctx context.Context
-
-	props propagation.TextMapPropagator
-
-	written     int64
-	statusCode  int
-	err         error
-	wroteHeader bool
-}
-
-func (w *respWriterWrapper) Header() http.Header {
-	return w.ResponseWriter.Header()
-}
-
-func (w *respWriterWrapper) Write(p []byte) (int, error) {
-	if !w.wroteHeader {
-		w.WriteHeader(http.StatusOK)
-	}
-	n, err := w.ResponseWriter.Write(p)
-	n1 := int64(n)
-	w.record(n1)
-	w.written += n1
-	w.err = err
-	return n, err
-}
-
-// WriteHeader persists initial statusCode for span attribution.
-// All calls to WriteHeader will be propagated to the underlying ResponseWriter
-// and will persist the statusCode from the first call.
-// Blocking consecutive calls to WriteHeader alters expected behavior and will
-// remove warning logs from net/http where developers will notice incorrect handler implementations.
-func (w *respWriterWrapper) WriteHeader(statusCode int) {
-	if !w.wroteHeader {
-		w.wroteHeader = true
-		w.statusCode = statusCode
-	}
-	w.ResponseWriter.WriteHeader(statusCode)
-}
-
-func (w *respWriterWrapper) Flush() {
-	if !w.wroteHeader {
-		w.WriteHeader(http.StatusOK)
-	}
-
-	if f, ok := w.ResponseWriter.(http.Flusher); ok {
-		f.Flush()
-	}
-}


Test patch:
diff --git a/instrumentation/net/http/otelhttp/internal/request/body_wrapper_test.go b/instrumentation/net/http/otelhttp/internal/request/body_wrapper_test.go
new file mode 100644
index 000000000..794e54fb9
--- /dev/null
+++ b/instrumentation/net/http/otelhttp/internal/request/body_wrapper_test.go
@@ -0,0 +1,74 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package request
+
+import (
+	"errors"
+	"io"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+var errFirstCall = errors.New("first call")
+
+func TestBodyWrapper(t *testing.T) {
+	bw := NewBodyWrapper(io.NopCloser(strings.NewReader("hello world")), func(int64) {})
+
+	data, err := io.ReadAll(bw)
+	require.NoError(t, err)
+	assert.Equal(t, "hello world", string(data))
+
+	assert.Equal(t, int64(11), bw.BytesRead())
+	assert.Equal(t, io.EOF, bw.Error())
+}
+
+type multipleErrorsReader struct {
+	calls int
+}
+
+type errorWrapper struct{}
+
+func (errorWrapper) Error() string {
+	return "subsequent calls"
+}
+
+func (mer *multipleErrorsReader) Read([]byte) (int, error) {
+	mer.calls = mer.calls + 1
+	if mer.calls == 1 {
+		return 0, errFirstCall
+	}
+
+	return 0, errorWrapper{}
+}
+
+func TestBodyWrapperWithErrors(t *testing.T) {
+	bw := NewBodyWrapper(io.NopCloser(&multipleErrorsReader{}), func(int64) {})
+
+	data, err := io.ReadAll(bw)
+	require.Equal(t, errFirstCall, err)
+	assert.Equal(t, "", string(data))
+	require.Equal(t, errFirstCall, bw.Error())
+
+	data, err = io.ReadAll(bw)
+	require.Equal(t, errorWrapper{}, err)
+	assert.Equal(t, "", string(data))
+	require.Equal(t, errorWrapper{}, bw.Error())
+}
+
+func TestConcurrentBodyWrapper(t *testing.T) {
+	bw := NewBodyWrapper(io.NopCloser(strings.NewReader("hello world")), func(int64) {})
+
+	go func() {
+		_, _ = io.ReadAll(bw)
+	}()
+
+	assert.NotNil(t, bw.BytesRead())
+	assert.Eventually(t, func() bool {
+		return errors.Is(bw.Error(), io.EOF)
+	}, time.Second, 10*time.Millisecond)
+}
diff --git a/instrumentation/net/http/otelhttp/wrap_test.go b/instrumentation/net/http/otelhttp/internal/request/resp_writer_wrapper_test.go
similarity index 64%
rename from instrumentation/net/http/otelhttp/wrap_test.go
rename to instrumentation/net/http/otelhttp/internal/request/resp_writer_wrapper_test.go
index d4b89411a..21229b4dc 100644
--- a/instrumentation/net/http/otelhttp/wrap_test.go
+++ b/instrumentation/net/http/otelhttp/internal/request/resp_writer_wrapper_test.go
@@ -1,7 +1,7 @@
 // Copyright The OpenTelemetry Authors
 // SPDX-License-Identifier: Apache-2.0
 
-package otelhttp
+package request
 
 import (
 	"net/http"
@@ -12,10 +12,7 @@ import (
 )
 
 func TestRespWriterWriteHeader(t *testing.T) {
-	rw := &respWriterWrapper{
-		ResponseWriter: &httptest.ResponseRecorder{},
-		record:         func(int64) {},
-	}
+	rw := NewRespWriterWrapper(&httptest.ResponseRecorder{}, func(int64) {})
 
 	rw.WriteHeader(http.StatusTeapot)
 	assert.Equal(t, http.StatusTeapot, rw.statusCode)
@@ -26,10 +23,7 @@ func TestRespWriterWriteHeader(t *testing.T) {
 }
 
 func TestRespWriterFlush(t *testing.T) {
-	rw := &respWriterWrapper{
-		ResponseWriter: &httptest.ResponseRecorder{},
-		record:         func(int64) {},
-	}
+	rw := NewRespWriterWrapper(&httptest.ResponseRecorder{}, func(int64) {})
 
 	rw.Flush()
 	assert.Equal(t, http.StatusOK, rw.statusCode)
@@ -49,12 +43,21 @@ func (_ nonFlushableResponseWriter) Write([]byte) (int, error) {
 func (_ nonFlushableResponseWriter) WriteHeader(int) {}
 
 func TestRespWriterFlushNoFlusher(t *testing.T) {
-	rw := &respWriterWrapper{
-		ResponseWriter: nonFlushableResponseWriter{},
-		record:         func(int64) {},
-	}
+	rw := NewRespWriterWrapper(nonFlushableResponseWriter{}, func(int64) {})
 
 	rw.Flush()
 	assert.Equal(t, http.StatusOK, rw.statusCode)
 	assert.True(t, rw.wroteHeader)
 }
+
+func TestConcurrentRespWriterWrapper(t *testing.T) {
+	rw := NewRespWriterWrapper(&httptest.ResponseRecorder{}, func(int64) {})
+
+	go func() {
+		_, _ = rw.Write([]byte("hello world"))
+	}()
+
+	assert.NotNil(t, rw.BytesWritten())
+	assert.NotNil(t, rw.StatusCode())
+	assert.NoError(t, rw.Error())
+}