Description
Go version
go version go1.23.4 darwin/arm64
Output of go env
in your module/workspace:
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 andSetReadDeadline
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
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
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 #70834 (comment) 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:
Lines 2008 to 2010 in 8391579
It is canceled by the call to handleReadError
here:
Line 721 in 8391579
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
- net/http: client closes persistent connections when context deadline is exceeded without reuse #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.
- net/http: Setting r.Close = true in a handler does not cause the server to close the connection #70833 is probably just a docs issue, but was discovered while investigating this bug.