Skip to content

net/http: request.Body.Read() can be called after client.Do() returns and resp.Body is drained/closed #51907

Open
@liggitt

Description

@liggitt

What version of Go are you using (go version)?

$ go version
go version go1.18 darwin/amd64

Does this issue reproduce with the latest release?

Yes

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/liggitt/Library/Caches/go-build"
GOENV="/Users/liggitt/Library/Application Support/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOINSECURE=""
GOMODCACHE="/Users/liggitt/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="darwin"
GOPATH="/Users/liggitt/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/Users/liggitt/.gvm/gos/go1.18"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/Users/liggitt/.gvm/gos/go1.18/pkg/tool/darwin_amd64"
GOVCS=""
GOVERSION="go1.18"
GCCGO="gccgo"
GOAMD64="v1"
AR="ar"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD="/Users/liggitt/go/src/k8s.io/kubernetes/go.mod"
GOWORK=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -arch x86_64 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/7f/9xt_73f12xlby0w362rgk0s400kjgb/T/go-build2769859399=/tmp/go-build -gno-record-gcc-switches -fno-common"

What did you do?

Make a request with a resettable body, if the response is a 429, reset the body and repeat the request.

Reported in kubernetes/kubernetes#108906, standalone reproducer here:

package mytest

import (
	"bytes"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

func TestSeek(t *testing.T) {
	testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		req.Body.Close()
		http.Error(w, "try again", http.StatusTooManyRequests)
	}))
	defer testServer.Close()

	client := &http.Client{}

	body := bytes.NewReader([]byte(strings.Repeat("abcd", 1000)))
	req, err := http.NewRequest("POST", testServer.URL, body)
	if err != nil {
		t.Fatal(err)
	}

	for i := 0; i < 1000; i++ {
		resp, err := client.Do(req)
		if err != nil {
			t.Fatal(err)
		}
		// drain and close the response body to complete the request before reusing the request body
		io.Copy(io.Discard, resp.Body)
		resp.Body.Close()
		body.Seek(0, 0)
	}
}

What did you expect to see?

Success, as in go1.17 and earlier releases

$ go version && go test -c -race my_test.go && stress ./mytest.test 

go version go1.17.8 darwin/amd64
72 runs so far, 0 failures
151 runs so far, 0 failures
214 runs so far, 0 failures
...
3084 runs so far, 0 failures
^C

What did you see instead?

Data race between the body reset and reads from the request body called from net/http.(*transferWriter).writeBody()

$ go version && go test -c -race my_test.go && stress ./mytest.test 

go version go1.18 darwin/amd64
73 runs so far, 0 failures
152 runs so far, 0 failures

/var/folders/7f/9xt_73f12xlby0w362rgk0s400kjgb/T/go-stress-20220325T130052-929070282
==================
WARNING: DATA RACE
Read at 0x00c00007ef78 by goroutine 13:
  bytes.(*Reader).Read()
      /Users/liggitt/.gvm/gos/go1.18/src/bytes/reader.go:41 +0x58
  io.(*nopCloser).Read()
      <autogenerated>:1 +0x76
  io.discard.ReadFrom()
      /Users/liggitt/.gvm/gos/go1.18/src/io/io.go:610 +0x91
  io.(*discard).ReadFrom()
      <autogenerated>:1 +0x5d
  io.copyBuffer()
      /Users/liggitt/.gvm/gos/go1.18/src/io/io.go:412 +0x1c2
  io.Copy()
      /Users/liggitt/.gvm/gos/go1.18/src/io/io.go:385 +0x64
  net/http.(*transferWriter).doBodyCopy()
      /Users/liggitt/.gvm/gos/go1.18/src/net/http/transfer.go:411 +0x3e
  net/http.(*transferWriter).writeBody()
      /Users/liggitt/.gvm/gos/go1.18/src/net/http/transfer.go:374 +0x7ef
  net/http.(*Request).write()
      /Users/liggitt/.gvm/gos/go1.18/src/net/http/request.go:698 +0x1197
  net/http.(*persistConn).writeLoop()
      /Users/liggitt/.gvm/gos/go1.18/src/net/http/transport.go:2395 +0x2e4
  net/http.(*Transport).dialConn.func6()
      /Users/liggitt/.gvm/gos/go1.18/src/net/http/transport.go:1751 +0x39

Previous write at 0x00c00007ef78 by goroutine 91:
  bytes.(*Reader).Read()
      /Users/liggitt/.gvm/gos/go1.18/src/bytes/reader.go:46 +0x11c
  io.(*nopCloser).Read()
      <autogenerated>:1 +0x76
  io.(*LimitedReader).Read()
      /Users/liggitt/.gvm/gos/go1.18/src/io/io.go:476 +0xc5
  io.copyBuffer()
      /Users/liggitt/.gvm/gos/go1.18/src/io/io.go:426 +0x28a
  io.Copy()
      /Users/liggitt/.gvm/gos/go1.18/src/io/io.go:385 +0x8d
  net.genericReadFrom()
      /Users/liggitt/.gvm/gos/go1.18/src/net/net.go:662 +0x28
  net.(*TCPConn).readFrom()
      /Users/liggitt/.gvm/gos/go1.18/src/net/tcpsock_posix.go:54 +0xac
  net.(*TCPConn).ReadFrom()
      /Users/liggitt/.gvm/gos/go1.18/src/net/tcpsock.go:130 +0x68
  io.copyBuffer()
      /Users/liggitt/.gvm/gos/go1.18/src/io/io.go:412 +0x1c2
  io.Copy()
      /Users/liggitt/.gvm/gos/go1.18/src/io/io.go:385 +0x86
  net/http.persistConnWriter.ReadFrom()
      /Users/liggitt/.gvm/gos/go1.18/src/net/http/transport.go:17
…

/var/folders/7f/9xt_73f12xlby0w362rgk0s400kjgb/T/go-stress-20220325T130052-124085153
==================
WARNING: DATA RACE
Read at 0x00c0000a0f78 by goroutine 13:
  bytes.(*Reader).Read()
      /Users/liggitt/.gvm/gos/go1.18/src/bytes/reader.go:41 +0x58
  io.(*nopCloser).Read()
      <autogenerated>:1 +0x76
  io.discard.ReadFrom()
      /Users/liggitt/.gvm/gos/go1.18/src/io/io.go:610 +0x91
  io.(*discard).ReadFrom()
      <autogenerated>:1 +0x5d
  io.copyBuffer()
      /Users/liggitt/.gvm/gos/go1.18/src/io/io.go:412 +0x1c2
  io.Copy()
      /Users/liggitt/.gvm/gos/go1.18/src/io/io.go:385 +0x64
  net/http.(*transferWriter).doBodyCopy()
      /Users/liggitt/.gvm/gos/go1.18/src/net/http/transfer.go:411 +0x3e
  net/http.(*transferWriter).writeBody()
      /Users/liggitt/.gvm/gos/go1.18/src/net/http/transfer.go:374 +0x7ef
  net/http.(*Request).write()
      /Users/liggitt/.gvm/gos/go1.18/src/net/http/request.go:698 +0x1197
  net/http.(*persistConn).writeLoop()
      /Users/liggitt/.gvm/gos/go1.18/src/net/http/transport.go:2395 +0x2e4
  net/http.(*Transport).dialConn.func6()
      /Users/liggitt/.gvm/gos/go1.18/src/net/http/transport.go:1751 +0x39

Previous write at 0x00c0000a0f78 by goroutine 92:
  bytes.(*Reader).Read()
      /Users/liggitt/.gvm/gos/go1.18/src/bytes/reader.go:46 +0x11c
  io.(*nopCloser).Read()
      <autogenerated>:1 +0x76
  io.(*LimitedReader).Read()
      /Users/liggitt/.gvm/gos/go1.18/src/io/io.go:476 +0xc5
  io.copyBuffer()
      /Users/liggitt/.gvm/gos/go1.18/src/io/io.go:426 +0x28a
  io.Copy()
      /Users/liggitt/.gvm/gos/go1.18/src/io/io.go:385 +0x8d
  net.genericReadFrom()
      /Users/liggitt/.gvm/gos/go1.18/src/net/net.go:662 +0x28
  net.(*TCPConn).readFrom()
      /Users/liggitt/.gvm/gos/go1.18/src/net/tcpsock_posix.go:54 +0xac
  net.(*TCPConn).ReadFrom()
      /Users/liggitt/.gvm/gos/go1.18/src/net/tcpsock.go:130 +0x68
  io.copyBuffer()
      /Users/liggitt/.gvm/gos/go1.18/src/io/io.go:412 +0x1c2
  io.Copy()
      /Users/liggitt/.gvm/gos/go1.18/src/io/io.go:385 +0x86
  net/http.persistConnWriter.ReadFrom()
      /Users/liggitt/.gvm/gos/go1.18/src/net/http/transport.go:17
…
213 runs so far, 2 failures

Metadata

Metadata

Assignees

No one assigned

    Labels

    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions