Uh oh!
There was an error while loading. Please reload this page.
Fail the write when the peer is gone and fwrite() reports nothing - #116
Fail the write when the peer is gone and fwrite() reports nothing#116wtsergo wants to merge 1 commit into
Conversation
6e226bc to
699c051Comparewtsergo
commented
Aug 21, 2026
CI is red here, but not on anything this PR touches — flagging it so it doesn't cost you a review cycle. The only failing test in every run is the pre-existing It got a complete HTTP response back, so the 256 KiB TLS write itself succeeded — the assertion fails purely on the response body. Nothing in this change can produce that: the new branch only fires when a write makes no progress on a stream that is already at EOF.
The other jobs are not independent failures — Windows and macOS never ran tests, their logs end at The new test does run and pass on your Ubuntu runners. In the progress line Happy to rebase, or to split out a fix that makes |
Fixes#115.
WritableResourceStream'sonWritablecallback treats a write as failed only whenfwrite()returned zero and a PHP error was captured:On a TLS stream whose peer is gone that is not sufficient on every OpenSSL build.
fwrite()returnsint(0)— neverfalse— and once the stream's EOF flag has been set, some builds stop reporting the failure, leaving$errorCodeat0whilewrite(2)still returnsEPIPE. The failed write is then mistaken for a partial write, the chunk is unshifted back onto the queue and the callback returns. Since thefinallyonly disables or cancels the watcher when$writesis empty, the writable watcher is left enabled on a dead descriptor — permanently writable to the event loop — and the callback re-fires forever.The fiber blocked in
write()is never resumed, so nothing downstream ever gets the chance to close the socket; the descriptor leaks along with the spin.What sets the EOF flag is
ReadableResourceStream's own read callback ($data === '' && \feof($stream)) on the same underlying resource — so a server that simply reads its client to EOF is what silences the write side.Changes
src/WritableResourceStream.php— also treat a no-progress write on a stream at EOF as a failure, and word the exception accordingly when no error was reported.test/WritableResourceStreamTest.php— regression test: establish a TLS connection over loopback, close the peer, read to EOF, then write. Without the fix the write never returns and the test hits its 5s limit; with it,write()throwsStreamException.Because the underlying
fwrite()behaviour is build-dependent, the test probes it first and skips where the platform still reports failed writes — on those builds the existing$errorCodecheck already catches it and there is nothing to exercise:fwrite()after read-to-EOFret=0, nothing reportedret=0, reports every timeOn an affected build, without the fix:
and with it:
OK (1 test, 3 assertions).Notes on the condition
$firstWriteis deliberately kept, so the macOS/FreeBSD buffer-refill case the original guard was written for is unaffected — and on a genuinely dead socket no earlier chunk in the same invocation can have succeeded, so it is alwaystruethere.The shape worth considering is a client that half-closes its write side while still reading, since
\feof()is true there on a perfectly writable socket. That is safe, because the callback only runs when the loop reports writability, so a zero-byte write inside it already means something is wrong. I verified it by pushing 2 MiB to such a client through a 2 KiB receive window with a deliberately slow reader: every byte arrives, with and without this change.Full suite passes (139 tests, 595 assertions). The one failure I see,
testUploadBiggerBlockSecure, is unrelated — it talks to the livehttpbin.org, which currently answers503 Service Temporarily Unavailable.