|
18 | 18 | */ |
19 | 19 | #include "../test.hpp" |
20 | 20 |
|
| 21 | +#include <future> |
| 22 | + |
21 | 23 | BOOST_AUTO_TEST_SUITE(socket_tests) |
22 | 24 |
|
23 | 25 | class socket_accessor |
@@ -231,4 +233,115 @@ BOOST_AUTO_TEST_CASE(socket__write__disconnected__bad_stream) |
231 | 233 | BOOST_REQUIRE(pool.join()); |
232 | 234 | } |
233 | 235 |
|
| 236 | +// Regression test for a fix to socket::async_write: it used to write every |
| 237 | +// body_write() chunk as its own whole websocket message (finish always |
| 238 | +// true), splitting one logical response into N independent messages. The |
| 239 | +// fix passes finish = !out->more, so only the final chunk closes the |
| 240 | +// message. A real (non-library) websocket client is used as ground truth: |
| 241 | +// beast's blocking read() returns only once a full message (finish) is |
| 242 | +// received, so a single read() call assembling the entire multi-chunk body |
| 243 | +// proves the chunks were sent as one multi-frame message rather than as |
| 244 | +// several. |
| 245 | +BOOST_AUTO_TEST_CASE(socket__body_write__websocket_multiple_chunks__single_message) |
| 246 | +{ |
| 247 | + using namespace std::chrono_literals; |
| 248 | + |
| 249 | + const logger log{}; |
| 250 | + threadpool pool(2); |
| 251 | + connector::parameters params{ .maximum_request = 1'000'000u }; |
| 252 | + |
| 253 | + // Bind a loopback acceptor on an ephemeral port. |
| 254 | + asio::strand accept_strand(pool.service().get_executor()); |
| 255 | + asio::acceptor acceptor(accept_strand); |
| 256 | + boost_code ec{}; |
| 257 | + const asio::endpoint bind_endpoint(asio::ipv4::loopback(), 0); |
| 258 | + |
| 259 | + acceptor.open(bind_endpoint.protocol(), ec); |
| 260 | + BOOST_REQUIRE(!ec); |
| 261 | + acceptor.set_option(asio::reuse_address(true), ec); |
| 262 | + BOOST_REQUIRE(!ec); |
| 263 | + acceptor.bind(bind_endpoint, ec); |
| 264 | + BOOST_REQUIRE(!ec); |
| 265 | + acceptor.listen(1, ec); |
| 266 | + BOOST_REQUIRE(!ec); |
| 267 | + const auto port = acceptor.local_endpoint().port(); |
| 268 | + |
| 269 | + const auto server = std::make_shared<socket_accessor>(log, pool.service(), std::move(params)); |
| 270 | + const auto buffer = std::make_shared<http::flat_buffer>(); |
| 271 | + const auto request = std::make_shared<http::request>(); |
| 272 | + |
| 273 | + // Small size_hint relative to the payload forces writer.get() across |
| 274 | + // many passes, i.e. many socket::async_write calls for one logical body. |
| 275 | + json::body<>::value_type content{}; |
| 276 | + content.model = boost::json::object{ { "key", std::string(4000, 'x') } }; |
| 277 | + content.size_hint = 32; |
| 278 | + const auto expected = boost::json::serialize(content.model); |
| 279 | + |
| 280 | + const auto response = std::make_shared<http::response>(); |
| 281 | + response->result(boost::beast::http::status::ok); |
| 282 | + response->body() = std::move(content); |
| 283 | + |
| 284 | + const auto upgrade_result = std::make_shared<std::promise<code>>(); |
| 285 | + const auto write_result = std::make_shared<std::promise<code>>(); |
| 286 | + auto upgrade_future = upgrade_result->get_future(); |
| 287 | + auto write_future = write_result->get_future(); |
| 288 | + |
| 289 | + server->accept(acceptor, |
| 290 | + [=](const code& accept_ec) mutable |
| 291 | + { |
| 292 | + if (accept_ec) |
| 293 | + { |
| 294 | + upgrade_result->set_value(accept_ec); |
| 295 | + write_result->set_value(accept_ec); |
| 296 | + return; |
| 297 | + } |
| 298 | + |
| 299 | + server->http_read(*buffer, *request, |
| 300 | + [=](const code& read_ec, size_t) mutable |
| 301 | + { |
| 302 | + upgrade_result->set_value(read_ec); |
| 303 | + if (read_ec != error::upgraded) |
| 304 | + { |
| 305 | + write_result->set_value(read_ec); |
| 306 | + return; |
| 307 | + } |
| 308 | + |
| 309 | + server->body_write(std::move(*response), |
| 310 | + [=](const code& write_ec, size_t) NOEXCEPT |
| 311 | + { |
| 312 | + write_result->set_value(write_ec); |
| 313 | + }); |
| 314 | + }); |
| 315 | + }); |
| 316 | + |
| 317 | + // Real (blocking) websocket client, independent of the code under test. |
| 318 | + asio::context client_service; |
| 319 | + asio::socket raw(client_service); |
| 320 | + boost_code client_ec{}; |
| 321 | + raw.connect({ asio::ipv4::loopback(), port }, client_ec); |
| 322 | + BOOST_REQUIRE(!client_ec); |
| 323 | + |
| 324 | + ws::socket client(std::move(raw)); |
| 325 | + client.handshake("127.0.0.1", "/", client_ec); |
| 326 | + BOOST_REQUIRE(!client_ec); |
| 327 | + |
| 328 | + // One blocking read of a "complete message" must assemble every chunk. |
| 329 | + http::flat_buffer read_buffer{}; |
| 330 | + client.read(read_buffer, client_ec); |
| 331 | + BOOST_REQUIRE(!client_ec); |
| 332 | + BOOST_REQUIRE(client.got_text()); |
| 333 | + |
| 334 | + const auto received = boost::beast::buffers_to_string(read_buffer.data()); |
| 335 | + BOOST_REQUIRE_EQUAL(received, expected); |
| 336 | + |
| 337 | + BOOST_REQUIRE(upgrade_future.wait_for(2s) == std::future_status::ready); |
| 338 | + BOOST_REQUIRE_EQUAL(upgrade_future.get(), error::upgraded); |
| 339 | + BOOST_REQUIRE(write_future.wait_for(2s) == std::future_status::ready); |
| 340 | + BOOST_REQUIRE_EQUAL(write_future.get(), error::success); |
| 341 | + |
| 342 | + server->stop(); |
| 343 | + pool.stop(); |
| 344 | + BOOST_REQUIRE(pool.join()); |
| 345 | +} |
| 346 | + |
234 | 347 | BOOST_AUTO_TEST_SUITE_END() |
0 commit comments