Component
Instrumentation: otelhttp
Describe the issue you're facing
Hey Otel team,
I'm actually not 100% sure if this is expected or not, but I still wanted to report it. I also couldn't find another issue that seemed related.
Analysis
We are using a retrying roundtripper that uses the otelhttp roundtripper. I'm currently upgrading from quite an old OTEL version otelhttp@v0.46.1 to v0.67.0 and I noticed that our tests were suddenly failing.
I took some time to investigate and I think I found the issue:
The roundtripper creates a wrapper around the body.
|
var lastBW *request.BodyWrapper // Records the last body wrapper. Can be nil. |
|
maybeWrapBody := func(body io.ReadCloser) io.ReadCloser { |
|
if body == nil || body == http.NoBody { |
|
return body |
|
} |
|
bw := request.NewBodyWrapper(body, func(int64) {}) |
|
lastBW = bw |
|
return bw |
|
} |
|
r.Body = maybeWrapBody(r.Body) |
|
if r.GetBody != nil { |
|
originalGetBody := r.GetBody |
|
r.GetBody = func() (io.ReadCloser, error) { |
|
b, err := originalGetBody() |
|
if err != nil { |
|
lastBW = nil // The underlying transport will fail to make a retry request, hence, record no data. |
|
return nil, err |
|
} |
|
return maybeWrapBody(b), nil |
|
} |
|
} |
The net/http library has a fallback to rewind a body in case nothing was written:
https://github.com/golang/go/blob/56ebf80e57db9f61981fc0636fc6419dc6f68eda/src/net/http/transport.go#L2225-L2229
When nothing was written it returns a nothingWrittenError{err} which actually causes the request body to be rewinded later in the logic.
With the wrapped body the check fails at: https://github.com/golang/go/blob/56ebf80e57db9f61981fc0636fc6419dc6f68eda/src/net/http/transfer.go#L106-L108
The wrapped body is not a known type checked in isKnownInMemoryReader(t.Body), which sets t.FlushHeaders = true.
On the next retry request, even though the seek index is at the end of the body, we send another request that only contains the headers with a content length > 0 but an empty content.
The connection then writes the header bytes to the written bytes counter which causes a mismatch in the error mapping logic and instead of returning a nothingWrittenError we get a non rewindable error.
Mitigation
In my case the solution was quite simple. I simply clone the request body and rewind it myself before calling the underlying RoundTripper again
func cloneRequestBody(req *http.Request) error {
if req.Body == nil || req.Body == http.NoBody {
return nil
}
if req.GetBody == nil {
return nil
}
clonedBody, err := req.GetBody()
if err != nil {
return err
}
req.Body = clonedBody
return nil
}
Expected behavior
I expected the net/http logic to rewind the request properly
Steps to Reproduce
We wrapped the otelhttp RoundTripper with this https://github.com/wundergraph/cosmo/blob/main/router/internal/retrytransport/retry_transport.go.
Operating System
MacOS Version 26.2
Device Architecture
ARM64
Go Version
1.25
Component Version
otelhttp@v0.67.0
Component
Instrumentation: otelhttp
Describe the issue you're facing
Hey Otel team,
I'm actually not 100% sure if this is expected or not, but I still wanted to report it. I also couldn't find another issue that seemed related.
Analysis
We are using a retrying roundtripper that uses the otelhttp roundtripper. I'm currently upgrading from quite an old OTEL version
otelhttp@v0.46.1tov0.67.0and I noticed that our tests were suddenly failing.I took some time to investigate and I think I found the issue:
The roundtripper creates a wrapper around the body.
opentelemetry-go-contrib/instrumentation/net/http/otelhttp/transport.go
Lines 119 to 139 in 17eafa1
The net/http library has a fallback to rewind a body in case nothing was written:
https://github.com/golang/go/blob/56ebf80e57db9f61981fc0636fc6419dc6f68eda/src/net/http/transport.go#L2225-L2229
When nothing was written it returns a
nothingWrittenError{err}which actually causes the request body to be rewinded later in the logic.With the wrapped body the check fails at: https://github.com/golang/go/blob/56ebf80e57db9f61981fc0636fc6419dc6f68eda/src/net/http/transfer.go#L106-L108
The wrapped body is not a known type checked in
isKnownInMemoryReader(t.Body), which setst.FlushHeaders = true.On the next retry request, even though the seek index is at the end of the body, we send another request that only contains the headers with a content length > 0 but an empty content.
The connection then writes the header bytes to the written bytes counter which causes a mismatch in the error mapping logic and instead of returning a
nothingWrittenErrorwe get a non rewindable error.Mitigation
In my case the solution was quite simple. I simply clone the request body and rewind it myself before calling the underlying RoundTripper again
Expected behavior
I expected the net/http logic to rewind the request properly
Steps to Reproduce
We wrapped the otelhttp RoundTripper with this https://github.com/wundergraph/cosmo/blob/main/router/internal/retrytransport/retry_transport.go.
Operating System
MacOS Version 26.2
Device Architecture
ARM64
Go Version
1.25
Component Version
otelhttp@v0.67.0