-
Notifications
You must be signed in to change notification settings - Fork 17.8k
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
net/http: ResponseWriter.ReadFrom writes to HEAD response body #68609
Comments
#19895 is one of the only matches for "Unsolicited response received on idle HTTP channel starting with" and claims the HTTP server to be misbehaving. |
Your server is indeed misbehaving by writing data when it receives a |
Sure, but it should not cause this sort of behavior at all. It's quite problematic, and inconsistent. Under many circumstances, there are no issues with writing a response to a HEAD request, even if it's wrong. It should not affect other connections to the server, which it does, and it should not leak data between connections, which it does. Please can this be reopened and considered more fairly? |
@seankhliao where is that documented in the net/http docs? A quick skim doesn't show me anything, even if HEAD responses not containing a body could be common knowledge. Assuming you're right about that assumption, I would imagine that calling Write on https://pkg.go.dev/net/http#ResponseWriter for a HEAD request should result in either a clear error or panic, and not anything like corruption or data races or confusing errors. |
To me the issue here looks like a Go HTTP client receiving a body in response to an HEAD request, and then the established HTTP/1.1 connection was reused for another request, where it tried to read the previously sent body as the start of the new response. This could happen regardless of the server implementation. RFC 9110 section 9.3.2. recommends clients close and error when a HEAD response is received with a body to avoid request/response smuggling. Is that something we can consider here? |
looks like the client (transport) assumes a HEAD request never has a body https://go.googlesource.com/go/+/3959d54c0bd5c92fe0a5e33fedb0595723efc23b/src/net/http/transport.go#2245 cc @neild |
I suppose #62015 would be the corresponding issue for discarding the body from the server side |
Thanks for the additional input, and thank you for reopening. Something I'd really like to understand is why this doesn't happen when writing data in-memory? I could only ever reproduce this when making a HTTP request on the server and writing it back to the client. |
Some of the above is talking about a body in a HEAD request, and some is talking about a body in the response to a HEAD request. These are completely different things. HEAD requests may contain a body, but the body has no defined semantics. (RFC 9110, section 9.3.2) The response to a HEAD request does not contain a body. RFC 9112, section 6.3:
Improperly writing a body leaves a connection in an invalid state, since the client is expecting the next bytes read to be the status line for the next request on the connection. Thanks for the report, @uhthomas. This is a bug in net/http. The problem is that while an |
@neild Ohh, that answers my previous question, thank you so much for the insight. |
Sorry, other than my mis-titling I think everything else was about bodies in the response to HEAD. Should the client / transport need to defend against misbehaving servers? |
The client does defend against misbehaving servers--that's the "Unsolicited response received on idle HTTP channel" error, which indicates that the server sent something unexpected. Perhaps the error message could be clearer; in this case the bytes read on the idle connection aren't a response, It's not something that should show up often, though, because a server needs to be quite broken for this to happen. |
@neild Given this error on the client, I don't think it's doing a very good job of defending against this? 2024/07/26 16:17:18 do: Head "http://localhost:8080": net/http: HTTP/1.x transport connection broken: malformed HTTP status code "html>" |
What do you think the client should have done in that case? It sent a request, and the server sent an invalid response (which happens to be the body of the previous response). |
Change https://go.dev/cl/601475 mentions this issue: |
In my testing the response was sent as part of the original request, so it was sent before the client sent a second request. As mentioned in #68609 (comment) I'd expect a reject and a connection close by the client. |
Perhaps around here the client could check if there was still buffered content and if so mark it as connection close? |
The client will read the extra data and close the connection (that's the "Unsolicited response..." error). If the client is immediately sending another request on the connection, there's a race condition between seeing the extra data and sending the next request. I don't think it's worth trying to detect this, because there will still be a race condition--there's no way to assert "the server didn't send us anything", because the data it improperly sent might still be in flight. And this only comes up if a server is completely broken. (Which, alas, net/http is.)
Seems fairly likely to me; it's not uncommon for a server to send headers in one packet and the body in another. In fact, that's exactly what the reproduction case here is doing (flush headers, then copy body). |
I think given http.Client is used for reverse proxies, and may be connecting to untrusted servers, we should exercise caution and do the right thing here. |
This comment was marked as spam.
This comment was marked as spam.
I tried a couple of things in an attempt to simplify my original reproducer and found that I cannot reproduce the if rf, ok := w.(io.ReaderFrom); ok {
n, err := rf.ReadFrom(io.LimitReader(rand.Reader, 1<<10))
fmt.Printf("%d: %d bytes written, err=%v\n", requestID, n, err)
return
} It does however produce the message instantly with: res, err := http.Get("https://go.dev")
if err != nil {
panic(err)
}
if rf, ok := w.(io.ReaderFrom); ok {
n, err := rf.ReadFrom(res.Body)
fmt.Printf("%d: %d bytes written, err=%v\n", requestID, n, err)
return
} Any ideas on why that could be the case? I also now understand why I couldn't reproduce it with this snippet: io.Copy(w, strings.NewReader(strings.Repeat("some string", 10)))
|
@neild Should we reopen this to track work in the HTTP client / transport? I really feel it should be a bit more defensive. |
I've raised #68653 to track the HTTP client. |
Go version
go version go1.22.5 linux/amd64
Output of
go env
in your module/workspace:What did you do?
I have published a small reproducer to this repository: https://github.com/uhthomas/go-unsolicited-http
The instructions are in the README, but for brevity:
Run the server:
code
main.go
Run the client:
code
main.go
What did you see happen?
Server:
Client:
What did you expect to see?
There should be no
Unsolicited response received on idle HTTP channel starting with
messages, and the request should not fail withnet/http: HTTP/1.x transport connection broken: malformed HTTP status code "html>"
.The text was updated successfully, but these errors were encountered: