When fwrite() could not place the whole response on the socket, the part
still to be sent was sliced out of the wrong string: $wret is an offset
into $response, which carries the http headers and the chunk framing
ahead of the content, but the slice was taken from $content. The client
was then served a chunk shorter than its declared length and without its
terminator, followed by bytes with no framing at all: the javascript
parser lost the @BEGIN@/@END@ boundaries and never found them again,
which is the comet stream falling apart. sac-a-push.phh already did this
right; brisk.phh and briskin5.phh did not.
Reproduced on a socket pair whose send buffer is smaller than the
payload: with the old slice the chunk declared 324012 bytes and only
323908 followed, with the new one the framing stays consistent and the
content comes out whole. It takes a response above the ~200 KB of the
socket buffer to show up, which is why a full room (240 players, ~18 KB
of bootstrap) never triggered it.
Two more defects of the same family:
- compress_chunk() re-wrote the whole input after a short write into the
deflate stream, instead of the part that was still missing, so the
client inflated a chunk carrying duplicated content.
- chunked_fini() returned "0\r\n" without the CRLF that closes the
trailer section, that is an unterminated last chunk. It has no callers
today (stream_close() does the work), but it was the same mistake
waiting to be made again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE