Skip to content

net/http: request context cancelled on readtimeout, persists across connection reuse  #70834

Open
@johnmaguire

Description

@johnmaguire

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 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 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:

go/src/net/http/server.go

Lines 2008 to 2010 in 8391579

ctx, cancelCtx := context.WithCancel(ctx)
c.cancelCtx = cancelCtx
defer cancelCtx()

It is canceled by the call to handleReadError here:

cr.handleReadError(err)

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

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

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions