-
Notifications
You must be signed in to change notification settings - Fork 18k
net/http: request context cancelled on readtimeout, persists across connection reuse #70834
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
The first related issue mentioned by @gabyhelp (#18447) is a bit interesting as it complains that the context should not be cancelled in the event of Server.ReadTimeout, as this is not mentioned in the docs for
Indeed, I did wonder if the error returned for a read deadline hit should be "context deadline exceeded" instead. Regardless, it does seem correct that the request (and its context) would be canceled when the deadline is hit, so perhaps the docs simply need adjustment. |
|
I believe the issue is that the ResponseController allows the request handler to modify the read timeout of the TCP connection in use by backgroundReader, which doesn't expect the timeout value to be anything other than 0. Stepping through the code in src/net/http/server.go:
A new context is created to track the lifetime of the series of requests made over the connection:
Note that this context is a parent context to every individual request context created over this connection:
Before the request is served, in some conditions, a background reader is started in a new goroutine:
The background reader sets the read deadline to 0 and invokes the background read in a new goroutine:
After the background reader is started, the ServeHTTP method is called:
In the example code provided in this ticket, the ServeHTTP method calls This new deadline interferes with the background reader, which hits the new read timeout set by the hander. Its error handling code ultimately cancels the outer context used as the parent of every request object context.
Despite this outer context now being cancelled, |
Change https://go.dev/cl/637715 mentions this issue: |
Thanks for the report. As @brad-defined says, the problem is that SetReadDeadline can apply a deadline to the connection after the background reader has started. (The background reader reads from a connection to detect connection errors after the request body has been consumed.) A secondary problem is that the server request read loop can continue to read from a connection after the connection has encountered a read error. For most errors this isn't a problem, since the second read attempt will fail. For timeouts, this is a problem, because resetting the deadline allows the next read to succeed. |
Go version
go version go1.23.4 darwin/arm64
Output of
go env
in your module/workspace:What did you do?
This code demonstrates an issue in Go HTTP/1.1 request handling when the following conditions are true:
ResponseController
is created andSetReadDeadline
is calledWhen 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 forfunc (*ResponseController) SetReadDeadline
state: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 forfunc (*Request) Context
are true: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
It is canceled by the call to
handleReadError
here:go/src/net/http/server.go
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 callingr.Header.Set("Connection", "close")
to ensure that the connection is not reused. We are also contemplating checking theContent-Length
header to ensure it's non-zero before callingSetReadDeadline
.Related Issues
The text was updated successfully, but these errors were encountered: