Go version

go version go1.24.3 darwin/amd64

Output of go env in your module/workspace:

AR='ar'
CC='clang'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='clang++'
GCCGO='gccgo'
GO111MODULE='auto'
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/tmp/.gocache'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/rittneje/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch x86_64 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/kf/kr7_s3xx0l12zbj3jrn082hmzy5gvy/T/go-build2339782732=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='amd64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD=''
GOMODCACHE='/Users/rittneje/go/pkg/mod'
GONOPROXY='[REDACTED]'
GONOSUMDB='[REDACTED]'
GOOS='darwin'
GOPATH='/Users/rittneje/go'
GOPRIVATE='[REDACTED]'
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/Users/rittneje/go1.24.3'
GOSUMDB='sum.golang.org'
GOTELEMETRY='off'
GOTELEMETRYDIR='/Users/rittneje/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='local'
GOTOOLDIR='/Users/rittneje/go1.24.3/pkg/tool/darwin_amd64'
GOVCS='[REDACTED]'
GOVERSION='go1.24.3'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

package main

import (
    "fmt"
    "io"
    "net/http"
    "net/http/httptest"
    "net/http/httputil"
    "strings"
)

func main() {

    s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
        b, err := httputil.DumpRequest(req, req.URL.Path == "/dump-body")
        if err != nil {
            panic(err)
        }
        fmt.Println("received request: " + string(b))

        req.Body.Close()
        fmt.Println(io.ReadAll(req.Body))
    }))
    defer s.Close()

    req, err := http.NewRequest(http.MethodGet, s.URL, strings.NewReader("blahblahblah"))
    if err != nil {
        panic(err)
    }

    if _, err := s.Client().Do(req); err != nil {
        panic(err)
    }

    fmt.Println("\n----------\n")

    req, err = http.NewRequest(http.MethodGet, s.URL+"/dump-body", strings.NewReader("blahblahblah"))
    if err != nil {
        panic(err)
    }
    req.Header.Set("Dump", "true")

    if _, err := s.Client().Do(req); err != nil {
        panic(err)
    }
}

What did you see happen?

For the first request, where the body isn't dumped, I see "http: invalid Read on closed Body", which corresponds to http.ErrBodyReadAfterClose.

For the second request, where the body is dumped, reading the body after closing it worked.

This means that code that works when httputil.DumpRequest is used may not work when it isn't.

What did you expect to see?

I expected httputil.DumpRequest to replace the body in such a way that it preserves the semantics described in the net/http documentation. Namely, it should still return http.ErrBodyReadAfterClose if you try to read the body after closing it.

Comment From: seankhliao

http.ErrBodyReadAfterClose is an error that may be returned on read after close, but not a guaranteed error, such as with other wrappers. The contract for Body is only that it is an io.ReadCloser.

Given that httputil.DumpRequest is only for debugging, I think this should be a won't fix.

Comment From: gabyhelp

Related Issues

Related Code Changes

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

Comment From: rittneje

@seankhliao This is a bug that can easily lead to code that only works when debug logging is enabled. If it's not going to be fixed, it should at least be mentioned as a potential hazard in the docs for httputil. And wrappers aren't really relevant to this, since they will end up observing the error anyway when the original body is closed.