Skip to content

Fix websocket body writes to frame as one multi-part message. - #844

Merged
evoskuil merged 1 commit into
libbitcoin:masterfrom
eynhaender:fix/chunking
Jul 26, 2026
Merged

Fix websocket body writes to frame as one multi-part message.#844
evoskuil merged 1 commit into
libbitcoin:masterfrom
eynhaender:fix/chunking

Conversation

@eynhaender

Copy link
Copy Markdown
Contributor

Summary

Fixes websocket body writes so a single logical response is delivered as one multi-frame websocket message instead of being split into several independent messages.

Problem

json::body<>::writer (and any other body writer) may need multiple get() passes to serialize a payload larger than its buffer/size_hint.

socket::do_body_write / do_body_notify loop over these passes and call socket::async_write once per chunk. Over TCP this is invisible (the stream has no message boundaries), but over websocket each call went through beast's stream::async_write, which always writes a complete message with fin set.

The result: an N-chunk serialization became N independent websocket messages instead of one message split across N frames — breaking any client that expects one WS message per logical response.

Fix

  • socket::async_write (socket.hpp / socket.cpp) now takes an explicit fin flag and, on the websocket path, calls beast's async_write_some(fin, buffer, handler) instead of the whole-message async_write.
  • do_body_write / do_body_notify (socket_body.cpp) pass fin = !out->more, so only the final chunk of a body closes the message.
  • do_ws_write (socket_ws.cpp), and tcp_write which routes through it, keep passing fin = true — they already hand off a single, complete buffer in one call, so behavior there is unchanged.
  • Confirmed against the Boost.Beast headers in this repo's prefix that binary()/text() only take effect at the start of a message (stream::binary docs), so calling it on every frame — as the code already did — remains safe with no changes needed there.

Testing

  • New unit test in test/messages/json_body_writer.cpp: forces a small size_hint relative to the payload so writer::get() must be called many times, and asserts the more sequence (true, ..., false) and that the reassembled chunks equal the full serialization.
  • New integration test in test/net/socket.cpp: a real loopback websocket connection where the server writes a multi-chunk JSON body and a raw Boost.Beast client (independent of the code under test) performs a single blocking read(). Beast's read() only returns once a complete message (fin) is received, so the test only passes if all chunks were sent as one multi-frame message — exactly the scenario this fix addresses.
  • Full suite run 3x with the fix: 1219/1219 passed. Baseline (pre-fix) run also passed 1219/1217 cleanly, confirming the fix introduces no regressions; two session_inbound_tests failures seen in one earlier run were reproduced as pre-existing flakes, not caused by this change.

socket::async_write now threads a fin flag through to beast's async_write_some instead of always writing a whole message. do_body_write and do_body_notify pass fin = !out->more, so a response body served across multiple writer.get() chunks is sent as a single multi-frame websocket message (fin set only on the final chunk) instead of N independent messages. ws_write/tcp_write keep fin = true, since they already hand off a single, already-whole buffer.
@evoskuil
evoskuil merged commit c1e9199 into libbitcoin:master Jul 26, 2026
13 of 28 checks passed
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.

2 participants