Skip to content

write() streams, appendBody() buffers, and five names that misreported (#180) - #182

Merged
EdmondDantes merged 14 commits into
mainfrom
180-body-rename
Aug 20, 2026
Merged

write() streams, appendBody() buffers, and five names that misreported (#180)#182
EdmondDantes merged 14 commits into
mainfrom
180-body-rename

Conversation

@EdmondDantes

Copy link
Copy Markdown
Contributor

Closes #180. Closes #181.

The renames (#180)

write() appended to a buffer and put nothing on the wire; the call that
streamed was send(). Node, Swoole and Go stream under the first name, and both
field reports behind this contract work (YanGusik/laravel-spawn#50, #60) came
from the API rather than from the adapter's code.

Before After
send(string): static write(string): static
write(string): static appendBody(string): static
isClosed(): bool isEnded(): bool
sendable(): bool tombstone: throws, naming isWritable() and tryWrite()/awaitWritable()
getBodyStream(), setBodyStream() removed

send() is removed rather than kept as an alias. It would have covered one call
in the shipped laravel-spawn adapter (src/Server/TrueAsyncServer.php:492) while
isClosed() breaks three others beside it at lines 106, 395 and 413 — the
adapter needs a release either way, so a second spelling of write() postpones
the same edit and carries a duplicate name through a minor. sendable() keeps
its declaration because its two replacements are not guessable from the name,
which is the one thing a tombstone is good for.

docs/USAGE.md gains §3.5, the three body modes with the state each commits and
how each is framed — the section deferred from #174 so it could be written once
against the final names.

The buffered body was discarded without error (#181)

setBody()/appendBody()/json()/html() followed by a streaming call put
only the streamed chunks on the wire. The streaming path commits its own headers
and never reads response->body, while the buffered dispose path runs only
while streaming is false. The reverse direction has always thrown, so a
symmetric-looking mistake reported on one side only. The guard every streaming
entry point shares now refuses a non-empty buffer and names both modes; an empty
one does not count, since setBody('') commits the handler to nothing.

Two things found in passing

tests/perf/servers/server_stream.php called ->send() with no argument against
an arginfo requiring one, so the stream perf profile answered 500 with
expects exactly 1 argument, 0 given before measuring anything, and its chunk
loop buffered through the old write(). Measured on the old file: 500 and 66
bytes; on the new one: 200 and 65536.

examples/sse-server.php broke its loop on !sendable() — the same read as
liveness that truncated the stream in #60, and which #174 removed from
README.md. It stops on !isWritable() now.

Evidence

tests/phpt/server/core/062-body-api-names.phpt reads the wire for each mode:
/buffered answers Content-Length: 7, /two-chunks and /streamed answer
chunked, /mixed answers Content-Length: 8 with the buffered body. Without the
#181 guard /mixed answers chunked with body streamed — the failing run is in
the commit message trail. h2/023-h2-sendable-tombstone.phpt asserts the throw
on a live HTTP/2 stream with a filling ring, which is where sendable() used to
return a meaningful answer; its former coverage of a refusal plus a byte-exact
hash is h2/025-h2-try-write.phpt in full.

tests/phpt/server/ + tests/phpt/websocket/: 346 passed, 0 failed, 24 skipped.

send() is removed rather than kept as an alias: it would have covered one
call in the shipped laravel-spawn adapter while isClosed(), removed in the
same release, breaks three others beside it. isClosed() becomes isEnded();
sendable() keeps its declaration and throws, naming isWritable() and
tryWrite()/awaitWritable(), because those two are not guessable from the
name; getBodyStream()/setBodyStream() are gone, neither having had an
implementation.

The stream perf profile called send() with no argument against an arginfo
requiring one, so it answered 500 before measuring anything.
setBody()/appendBody()/json()/html() followed by a streaming call put only
the streamed chunks on the wire: the streaming path commits its own headers
and the buffered dispose path runs only while streaming is false. The reverse
direction has always thrown, so the failure reported on one side only.

The guard every streaming entry point shares now refuses a non-empty buffer
and names both modes. An empty one does not count.
"waits for the write exactly as write() does" carried no fact after the
rename. The sendable() tombstone says why the declaration outlives the
method instead of restating what an undefined-method fatal looks like.
…#179)

Release PHP built for the run, three wrk runs per cell, median. strace
confirms three write(2) and three loop turns per chunk, flat in the chunk
size; a coalesced frame halves the per-chunk cost.

The win needs no queue, no second writer and no per-response structure, so
it is not an argument for the design #179 proposes.
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Coverage

Total lines: 82.22% → 82.12% (-0.10 pp)

File Baseline Current Δ Touched
src/compression/http_compression_response.c 80.14% 75.68% -4.47 pp
src/core/http_connection.c 75.45% 75.26% -0.19 pp
src/core/http_connection_tls.c 73.46% 73.74% +0.28 pp
src/core/worker_dispatch.c 78.43% 76.63% -1.80 pp
src/http1/http1_format.c 95.05% 95.05% +0.00 pp
src/http1/http1_stream.c 69.57% 66.23% -3.33 pp
src/http1/http_parser.c 84.06% 83.30% -0.76 pp
src/http2/http2_session.c 88.68% 88.68% +0.00 pp
src/http2/http2_strategy.c 83.33% 84.13% +0.79 pp
src/http3/http3_callbacks.c 82.62% 82.84% +0.22 pp
src/http3/http3_dispatch.c 82.77% 82.77% +0.00 pp
src/http3/http3_listener.c 78.08% 76.73% -1.35 pp
src/http3/http3_static_response.c 76.37% 76.37% +0.00 pp
src/http_response.c 86.11% 86.96% +0.85 pp
src/http_response_internal.h 100.00% 100.00% +0.00 pp
src/http_response_server_api.c 88.54% 88.54% +0.00 pp
src/http_server_class.c 75.07% 75.03% -0.04 pp
src/http_sse.c 85.11% 85.11% +0.00 pp
src/websocket/ws_session.c 90.41% 90.22% -0.18 pp

❌ Regression in touched files (> 1.0 pp drop)

  • src/compression/http_compression_response.c dropped -4.47 pp
  • src/core/worker_dispatch.c dropped -1.80 pp
  • src/http1/http1_stream.c dropped -3.33 pp

Add [coverage-drop-ok] to a commit message in this PR to override.

@EdmondDantes
EdmondDantes merged commit 02f5878 into main Aug 20, 2026
10 of 13 checks passed
@EdmondDantes
EdmondDantes deleted the 180-body-rename branch August 20, 2026 13:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A buffered body is discarded without error when the handler then streams The response body API names the streaming call write() for the buffered one

1 participant