Repository navigation
fix(http2): close the client connection when a write fails - #6391
Open
binaryfire wants to merge 1 commit into
Open
binaryfire wants to merge 1 commit into
binaryfire wants to merge 1 commit into
Conversation
When send_all() failed, Coroutine\Http2\Client::send() recorded the error but kept the connection. A timeout or cancel can leave part of a frame on the wire, and the server then reads every later frame as the rest of it, so nothing else on the connection is answered. The client still reported `connected`, and later send() calls returned stream ids. MultiplexClient's next request timed out the same way. A failed write now closes the connection and keeps the write's error. close() resumes a coroutine waiting in recv(), and that coroutine would otherwise replace the error with ECANCELED and then CLOSE_WAIT. The close needs three more changes to be complete and safe: - recv() finishes a close() that had to wait for its read. Before, the socket stayed open and connect() failed with EISCONN until close() was called again. - recv() and ping() hold the socket, as send(), write(), goaway() and close() already do. A writer woken by another coroutine's close() now finishes that close, which frees the socket while the outer close() is still running. - GOAWAY and an invalid SETTINGS_MAX_FRAME_SIZE now set their error after close(). A writer resumed by that close records its own error, which replaced the server's GOAWAY code and debug data. Also free the encoded form body when a send fails, and drop connect()'s second close() after a failed send, which replaced the error with EBADF.
binaryfire
force-pushed
the
fix/http2-client-failed-write-close
branch
from
November 12, 2026 04:13
1392d49 to
3c41e9c
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #6391 +/- ##
=======================================
Coverage 84.08% 84.08%
=======================================
Files 115 115
Lines 20237 20237
Branches 3637 3638 +1
=======================================
+ Hits 17016 17017 +1
+ Misses 3221 3220 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When a write fails,
Coroutine\Http2\Clientrecords the error but keeps the connection. A timeout or cancel can leave part of a frame on the wire. Later sends can then be read as its missing bytes, which corrupts the connection and leaves requests unanswered. The client still reportsconnectedand latersend()calls return stream ids, so callers wait out their read timeout.MultiplexClient's nextrequest()times out the same way. The HTTP/1 client already closes on every failed write.Fix
A failed write in
send(), including the queue flush, now closes the connection and keeps the write's error.close()resumes a coroutine waiting inrecv(), and that would otherwise replace the error with ECANCELED and then "Coroutine socket close wait".Closing from a failed write needs three more changes:
recv()finishes aclose()that had to wait for its read, asSocket::close()expects. Before, the socket stayed open andconnect()failed with EISCONN untilclose()was called again. This also applies when the user callsclose()while another coroutine is inrecv().recv()andping()hold the socket, assend(),write(),goaway()andclose()already do. A coroutine woken by another coroutine'sclose()can now finish that close and release the socket while the outerclose()is still running.SETTINGS_MAX_FRAME_SIZEset their error afterclose(). A writer resumed by that close records its own error, which replaced the server's GOAWAY code and debug data.Also, the encoded form body is freed when the write fails, and
connect()no longer callsclose()a second time after a failed send, which replaced the error with EBADF.Hypervel impact
Hypervel's
Engine\Http\V2\Clientwrapper passes send failures through and keeps using the native client, so after a failed send it still reports the connection as open and later requests on it go unanswered. Hypervel's regression test sends a 1 MB POST to a listener that never reads and checks that the client is disconnected after the write times out.Tests
close_on_send_failure.phptuses a peer that never reads:send()fails with no connection;recv()is woken, the socket is released, and the same client can connect again;It fails on master. Without the
recv()socket hold, the GOAWAY case segfaults.Also run: the HTTP/2 client and server suites, swoole/library's
MultiplexClientTest, the new test under Valgrind (which also checks that the form body is freed), and a ZTS build with thread support.