-
Notifications
You must be signed in to change notification settings - Fork 716
Description
Component
Instrumentation: otelhttp
Describe the issue you're facing
use case: retry http request on non 2xx status code or error
When reusing http.Request with a body that can be read only once, the subsequent request Do(*http.Request) fails with a formatted error (length and url may differ): http://127.0.0.1:62343": net/http: HTTP/1.x transport connection broken: http: ContentLength=5 with Body length
This suggests that the http client instrumentation attempts to ready body every time.
Expected behavior
No errors.
Request is issued every time with body.
The example code does not fail when the http client is not instrumented.
Steps to Reproduce
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"testing"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func Test_Reuse_Body_FAIL(t *testing.T) {
var bodies []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
bodies = append(bodies, string(body))
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
httpClient := &http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}
body := bytes.NewBuffer([]byte("hello"))
req, _ := http.NewRequestWithContext(t.Context(), http.MethodPost, server.URL, body)
for range 2 {
resp, err := httpClient.Do(req)
if err != nil {
t.Fatalf("failed to do http req: %v", err)
}
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)
}
if len(bodies) != 2 || bodies[0] != bodies[1] {
t.Fatal("test failed")
}
}Potential Fix
I am not 100% sure this the right approach but here's my shot:
replace direct access to Body field with GetBody() calls.
on
| bw := request.NewBodyWrapper(r.Body, func(int64) {}) |
Workaround
use bytes.Reader instead of bytes.Buffer. seek reader to start before every Do call.
body := bytes.NewReader([]byte("hello"))
req, _ := http.NewRequestWithContext(t.Context(), http.MethodPost, server.URL, body)
for range 2 {
body.Seek(0, io.SeekStart)
resp, err := httpClient.Do(req)
if err != nil {
t.Fatalf("failed to do http req: %v", err)
}
}Operating System
macos 15.7.2
Device Architecture
ARM64
Go Version
1.25
Component Version
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0