Repository navigation
Replies: 2 comments
TriageVerdict: a reproducible bug, not a misuse or a feature request. Over HTTP/2, a write that fails because the client closed its connection is logged as "response failed" whenever the failed write reaches the handler before the stream's context is cancelled. That happens without any test client quirk: it holds for every HTTP/2 client that goes away by closing its connection. HTTP/1.1 isn't affected. #1244 is filed to fix it. Is this a misuse of humus?No. The end user owns the WriterTo, and it does what the docs ask: it returns the write's error. humus owns what happens next. The end user can't defend against it. When the write fails, the request's context is still live, so the WriterTo has nothing to tell a client that went away from a failure of its own. Is it a feature request?No. The behaviour contradicts these promises:
A fix restores a promised behaviour. It changes no exported name, signature or default. Correcting Is it reproducible?Yes. At 2c3726e, with Go 1.27.0, under
Over HTTP/2, the reads held: the failed write is logged. The test wraps the Host's listener so that from the client's cancel, every read the server makes waits until the test releases it. The server then can't read the client's RST_STREAM or see the connection end. This forces the order the report describes. The client is
Over HTTP/1.1, the same: not logged. The write fails with the same error, but the context is already done: Unheld, the race is wider than the failures show. Across the report's repro's 3600 iterations, the write failed with the context still live 179 times. 151 of those were logged. In the other 28, the context was cancelled between the failed write and This confirms the report: 28 in 900 at 418776f is the same rate as the 33–48 in 900 I saw at 2c3726e. The existing test passes because its client keeps its connection, so the cancel resets only the stream, and nothing makes a write fail. WhyGo's HTTP/2 server runs each frame write on a goroutine of its own, and the serve loop handles its result in The serve loop also handles a pending write result before a frame it has read (L831-L839). So even when the client's reset reached the server first, the failed write is replied to first. I got that from Go's code and didn't reproduce it separately. HTTP/1.1 cancels the request's context in The same path is taken by any write after the status that fails on a closed HTTP/2 connection. That includes the catch-all's answer, which On the report's explanation: it holds. The reset doesn't have to arrive late for the failure to be logged, though: the serve loop orders a failed write ahead of a frame it has read. #1244 is filed to fix it:
|
|
Fixed by #1249 (story #1244), merged as 02f0286. The writer a WriterTo is handed now notes when a write or a flush fails because the connection did. A write the server refuses, a body for a status that has none or more bytes than the WriterTo's Len, still counts as the server's failure. A WriterTo failure is taken to be the client going away when the request's context is done, or when such a write failed. In that case it goes on the request's span alone, with the span's status set to Error, and no "response failed" record is emitted. So over HTTP/2, a write that fails on a closed connection before the server cancels the stream's context is no longer logged.
|
Uh oh!
There was an error while loading. Please reload this page.
What happens
Over HTTP/2, a client can cancel a streaming request and close its connection in one go. A client that keeps no idle connection does this, such as
httpservertest.NewClient. When it does, the WriterTo's next write can fail with the connection's error,io: read/write on closed pipeon humustest's Host, while the request's context is still live.route.logchecksctx.Err()to decide whether the client went away, so the failure is logged as an ERROR record, "response failed", even though the client went away. The doc says a write that fails because the client went away goes on the span alone.At 418776f, with Go 1.27, under
-race:TestAPI_records_a_failed_write_after_the_client_went_away_on_the_span_only/over_HTTP/2, with its HTTP/2 client changed back tos.client(no idle connection), fails 28 times in 900 (-count=300 -cpu 1,2,4). The error is "a write that failed because the client went away isn't logged", and the record'sexception.messageisio: read/write on closed pipe. #1123 changed the test's client to one that keeps its connection, so the cancel resets only the stream. That test passes 900 times in 900, but the race is still there.Repro, on top of 418776f, in
openapi/v3/stream_test.go:{"over HTTP/2", func(s *server) *http.Client { return s.client }},in place of the keep-alive client in that test, then:
Why
The client's RST_STREAM and its connection close reach the server together. HTTP/2's server runs the handler's frame writes on a goroutine of their own and cancels a stream's context only once its serve loop processes the reset or sees the connection end. A frame write that fails on the closed connection can return its error to the handler first. So
ctx.Err()is still nil whenabortcallslog.On a real network the same order is possible, and latency usually hides it, because the server reads the reset before its write fails. Before #1123, humustest's unbuffered
net.Pipehid it too: the client's close couldn't complete until the server had read it.Expected
A WriterTo's write that fails because the client went away, whether it reset the stream or closed the connection, goes on the span alone and isn't logged, as the doc of
abortandlogsays.References
openapi/v3/serve.go:route.abort,route.logAll reactions