Skip to content

net/http/httputil: DumpRequest replacement body allows read after close #74030

Closed as not planned
@rittneje

Description

@rittneje

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions