-
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
x/net/http2: response Write on HEAD fails with "http2: request body closed due to handler exiting" #66812
Comments
Looking into this a bit more, what seems to be happening is that Lines 6514 to 6516 in 519f6a0
However, it never gets that far. Instead, because Lines 6245 to 6246 in 519f6a0
That Lines 6401 to 6405 in 519f6a0
And that That's as far as I got for now. |
Ok, let's go deeper. We already mentioned that Line 6497 in 519f6a0
Let's now turn to Lines 4777 to 4778 in 519f6a0
Ok, so after writing each frame (we presume), it calls Line 5118 in 519f6a0
And Lines 5141 to 5142 in 519f6a0
And one of the things that Line 5499 in 519f6a0
Which ( Summarising: |
The following seems to be a fix – it fixes my example, at least: diff --git a/src/net/http/h2_bundle.go b/src/net/http/h2_bundle.go
index 75454dba38..397f21ce9d 100644
--- a/src/net/http/h2_bundle.go
+++ b/src/net/http/h2_bundle.go
@@ -6483,12 +6483,12 @@ func (rws *http2responseWriterState) writeChunk(p []byte) (n int, err error) {
contentLength: clen,
date: date,
})
+ if endStream && (err == nil || err == http2errStreamClosed) {
+ return len(p), nil
+ }
if err != nil {
return 0, err
}
- if endStream {
- return 0, nil
- }
}
if isHeadResp {
return len(p), nil |
If the maintainers agree that this fix goes in the right direction, I'd be happy to contribute it to the |
cc @neild |
You're sending a HEAD request to a handler that writes a response body. HEAD requests don't have a response body, of course, so the server needs to discard it. There seems to be some divergence in how the HTTP/1 and HTTP/2 servers deal with the case of a handler writing a body in response to a HEAD request. (None of this is in my working memory, so I'm going off of a quick glance at the code, and I may be getting some of the details wrong.) Both the HTTP/1 and HTTP/2 paths will buffer writes to the response body. If the entire body fits in the response buffer, then the server will send a Once the server has sent headers in response to a HEAD request, there's no point in the handler writing more of the response body--the server doesn't have anything to do with it, and all it can do is discard the bytes. The HTTP/1 The HTTP/2 I think returning an error is the better behavior here, since it lets the handler exit quickly. (Perhaps we should change the HTTP/1 server to do so as well, although Hyrum's Law probably applies here.) The error shouldn't lie about the handler being complete when it isn't, and ideally would explain what's going on: "stream closed" would be terse but accurate, "response body for HEAD request closed" might be more explanatory. I don't think suppressing the error is the right option, though. |
Fair point. However, there are also good arguments for why not returning an error is the right thing:
On balance, silently discarding writes for HTTP/2 (matching HTTP/1 behaviour) seems like the better way forward to me. Or if we want to return an error, then I feel like we should find some way to make this the behaviour for both HTTP/1 and HTTP/2 – which as you point out is a breaking change (albeit of undocumented behaviour) so requires care. |
But a naive GET handler does do the right thing for HEAD requests: The server responds to the HEAD request with appropriate headers. ResponseWriter.Write returning an error is a normal and expected condition, indicating that the handler can stop writing the response body because the client isn't reading it. This happens when a connection breaks mid-response (due to a network error, the client going away, or--in HTTP/2--the client canceling the request), or in this case when the response isn't being sent to the client at all. Changing the HTTP/2 server to silently discard writes seems like a strict degradation of functionality. |
True. Apart from the potentially confusing error trace in the logs (which is how I stumbled on this). Regarding your comparison with other errors returned from So yeah, it would certainly help to improve the error message wording. Something like "not writing data in response to HEAD request" maybe. Ideally, also introduce a dedicated
I agree, there are downsides to doing this (causing the handler to do unnecessary work, and being a change in behaviour). On the other hand, just tweaking the error message as suggested above also leaves the downside that (some) users will end up writing extra lines of code to suppress these non-error errors, and that it does not address (pre-existing) the inconsistency between HTTP/1 and HTTP/2. But given that I seem to be the first one reporting this, that may be a reasonable trade-off; it would certainly be the more conservative change. |
If the maintainers prefer the approach of merely tweaking the error message (and possibly exposing that |
Checking in here. Still happy to help out, once there is agreement from the maintainers on what the fix should look like. |
Go version
go version go1.22.2 linux/amd64
Output of
go env
in your module/workspace:What did you do?
Implement a trivial net/http Server that listens via HTTPS (using a self-signed cert for dev purposes) and responds with a fixed 5KB response:
What did you see happen?
When hitting the server with:
The response is as expected (i.e. code 200 but empty), but the server logs an error:
What did you expect to see?
No error logged.
Note that an error is only logged when using HTTP/2, and only when the response exceeds 4096 bytes.
Interestingly, a different error is logged if I remove the explicit setting of the Content-Type header (in which case the same header+value still ends up in the response, presumably because net/http auto-detects the content type):
The text was updated successfully, but these errors were encountered: