Repository: go
Issue: net/http: request context cancelled on readtimeout, persists across connection reuse 
Labels: NeedsInvestigation

### Go version

go version go1.23.4 darwin/arm64

### Output of `go env` in your module/workspace:

```shell
GO111MODULE=''
GOARCH='arm64'
GOBIN=''
GOCACHE='/Users/jmaguire/Library/Caches/go-build'
GOENV='/Users/jmaguire/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMODCACHE='/Users/jmaguire/go/pkg/mod'
GONOPROXY='github.com/DefinedNet'
GONOSUMDB='github.com/DefinedNet'
GOOS='darwin'
GOPATH='/Users/jmaguire/go'
GOPRIVATE='github.com/DefinedNet'
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/opt/homebrew/Cellar/go/1.23.4/libexec'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='local'
GOTOOLDIR='/opt/homebrew/Cellar/go/1.23.4/libexec/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.23.4'
GODEBUG=''
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/jmaguire/Library/Application Support/go/telemetry'
GCCGO='gccgo'
GOARM64='v8.0'
AR='ar'
CC='cc'
CXX='c++'
CGO_ENABLED='1'
GOMOD='/dev/null'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/2t/rbxbv8612sq9_rdrxhtw8rzc0000gn/T/go-build1060450768=/tmp/go-build -gno-record-gcc-switches -fno-common'
```


### What did you do?

This code demonstrates an issue in Go HTTP/1.1 request handling when the following conditions are true:

- An incoming request has an empty body
- A `ResponseController` is created and `SetReadDeadline` is called
- The time spent processing the request exceeds the read deadline

When these conditions are true, the HTTP server will erroneously cancel the request context. Additionally, a substantial percentage of future requests made on this HTTP connection will have their context immediately canceled.

To reproduce the issue, run the script found here: https://go.dev/play/p/wiX9Fyt2S2s

### What did you see happen?

The expected result is that both requests return 200 OK. Instead, both requests return 408 Request Timeout.

### What did you expect to see?

We expect a `200 OK` for the first request because the docs for [`func (*ResponseController) SetReadDeadline`](https://pkg.go.dev/net/http#ResponseController.SetReadDeadline) state:

> SetReadDeadline sets the deadline for reading the entire request, including the body. Reads from the request body after the deadline has been exceeded will return an error. A zero value means no deadline.
>
> Setting the read deadline after it has been exceeded will not extend it. 

Since the request contains no body to read, the read deadline should not have been exceeded.

A secondary problem is that when HTTP keep-alive is enabled, future requests made against this connection (which is not closed by the server) will also be immediately canceled when processing starts.

In the proof of concept, we expect a `200 OK` for the second request not only because is there no body to read, but this request uses a fast endpoint which returns well within the read deadline. Instead, the context is canceled as soon as we start processing the request. None of the conditions listed under the docs for [`func (*Request) Context`](https://pkg.go.dev/net/http#Request.Context) are true:

> For incoming server requests, the context is canceled when the client's connection closes, the request is canceled (with HTTP/2), or when the ServeHTTP method returns. 

While we have not root-caused the issue with the read deadline being exceeded when there is no body (update: see comment https://github.com/golang/go/issues/70834#issuecomment-2546088897 below), we believe that the connection is not closed correctly for this read error due to the this context being unchecked for cancellation in the `for` loop directly below: https://github.com/golang/go/blob/8391579ecea7dede2f2c1dc79954131e1eae1ade/src/net/http/server.go#L2008-L2010

It is canceled by the call to `handleReadError` here: https://github.com/golang/go/blob/8391579ecea7dede2f2c1dc79954131e1eae1ade/src/net/http/server.go#L721

In our testing, some requests _do_ succeed on the reused connection, but many fail with immediate context cancellation. Attempting to manually read the (nonexistent) body in the handler prior to the read deadline does not prevent context cancellation (e.g. `io.Copy(io.Discard, r.Body)`.) If the client sends any data in the body, this issue does not occur (set `-body` flag.) Additionally, the HTTP/2 server is not affected by the read deadline bug nor the cross-request state contamination bug (set `-http2` flag.)

### Workaround
We are catching `context.Canceled` and calling `r.Header.Set("Connection", "close")` to ensure that the connection is not reused. We are also contemplating checking the `Content-Length` header to ensure it's non-zero before calling `SetReadDeadline`.

### Related Issues
- https://github.com/golang/go/issues/45559 complains that the HTTP/2 server closes the connection to the client when the deadline is exceeded and the context is canceled. We believe that this is in fact the correct behavior, and HTTP/1.1 server should be doing this as well. Once a read error is hit, the connection should not be reused.
- https://github.com/golang/go/issues/70833 is probably just a docs issue, but was discovered while investigating this bug.