Skip to content

[Bug]: Unable to reuse http.Request with body when client is instrumented with otelhttp.NewTransport #8258

@adomaskizogian

Description

@adomaskizogian

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions