From acbcf299a033a85698cb67c8000c509b24bf02af Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Thu, 24 Jul 2025 09:20:31 +0200 Subject: [PATCH] Avoid copying buffers around when serializing HTTP response --- lib/remote/configfileshandler.cpp | 2 +- lib/remote/eventshandler.cpp | 2 +- lib/remote/httphandler.cpp | 2 +- lib/remote/httpmessage.cpp | 153 +++++++++++----- lib/remote/httpmessage.hpp | 265 ++++++--------------------- lib/remote/httpserverconnection.cpp | 14 +- lib/remote/infohandler.cpp | 13 +- lib/remote/mallocinfohandler.cpp | 2 +- test/CMakeLists.txt | 2 - test/remote-httpmessage.cpp | 51 +----- test/remote-httpserverconnection.cpp | 6 +- 11 files changed, 186 insertions(+), 326 deletions(-) diff --git a/lib/remote/configfileshandler.cpp b/lib/remote/configfileshandler.cpp index 401b205e2..2a17b46e7 100644 --- a/lib/remote/configfileshandler.cpp +++ b/lib/remote/configfileshandler.cpp @@ -82,7 +82,7 @@ bool ConfigFilesHandler::HandleRequest( response.result(http::status::ok); response.set(http::field::content_type, "application/octet-stream"); - response.body() << fp.rdbuf(); + response << fp.rdbuf(); } catch (const std::exception& ex) { HttpUtility::SendJsonError(response, params, 500, "Could not read file.", DiagnosticInformation(ex)); diff --git a/lib/remote/eventshandler.cpp b/lib/remote/eventshandler.cpp index b4132677f..21ddf6d22 100644 --- a/lib/remote/eventshandler.cpp +++ b/lib/remote/eventshandler.cpp @@ -114,7 +114,7 @@ bool EventsHandler::HandleRequest( if (event) { encoder.Encode(event); - response.body() << '\n'; + response << '\n'; response.Flush(yc); } } diff --git a/lib/remote/httphandler.cpp b/lib/remote/httphandler.cpp index 64cd15bf6..3656c7899 100644 --- a/lib/remote/httphandler.cpp +++ b/lib/remote/httphandler.cpp @@ -114,7 +114,7 @@ void HttpHandler::ProcessRequest( * in the middle of a streaming response. We can't send any error response, so the * only thing we can do is propagate it up. */ - if (response.HasSerializationStarted()) { + if (response.IsHeaderDone()) { throw; } diff --git a/lib/remote/httpmessage.cpp b/lib/remote/httpmessage.cpp index b3f1e32f5..79f872bd2 100644 --- a/lib/remote/httpmessage.cpp +++ b/lib/remote/httpmessage.cpp @@ -12,36 +12,25 @@ using namespace icinga; /** - * Adapter class for Boost Beast HTTP messages body to be used with the @c JsonEncoder. + * Adapter class for writing JSON data to an HTTP response. * - * This class implements the @c nlohmann::detail::output_adapter_protocol<> interface and provides - * a way to write JSON data directly into the body of a Boost Beast HTTP message. The adapter is designed - * to work with Boost Beast HTTP messages that conform to the Beast HTTP message interface and must provide - * a body type that has a publicly accessible `reader` type that satisfies the Beast BodyReader [^1] requirements. + * This class implements the @c AsyncJsonWriter interface and writes JSON data directly to the @c HttpResponse + * object. It uses the @c HttpResponse's output stream operator to fill the response body with JSON data. + * It is designed to be used with the @c JsonEncoder class to indirectly fill the response body with JSON + * data and then forward it to the underlying stream using the @c Flush() method whenever it needs to do so. * * @ingroup base - * - * [^1]: https://www.boost.org/doc/libs/1_85_0/libs/beast/doc/html/beast/concepts/BodyReader.html */ class HttpResponseJsonWriter : public AsyncJsonWriter { public: - explicit HttpResponseJsonWriter(HttpResponse& msg) : m_Reader(msg.base(), msg.body()), m_Message{msg} + explicit HttpResponseJsonWriter(HttpResponse& msg) : m_Message{msg} { - boost::system::error_code ec; - // This never returns an actual error, except when overflowing the max - // buffer size, which we don't do here. - m_Reader.init(m_MinPendingBufferSize, ec); - ASSERT(!ec); } ~HttpResponseJsonWriter() override { - boost::system::error_code ec; - // Same here as in the constructor, all the standard Beast HTTP message reader implementations - // never return an error here, it's just there to satisfy the interface requirements. - m_Reader.finish(ec); - ASSERT(!ec); + m_Message.Finish(); } void write_character(char c) override @@ -51,30 +40,27 @@ public: void write_characters(const char* s, std::size_t length) override { - boost::system::error_code ec; - boost::asio::const_buffer buf{s, length}; - while (buf.size()) { - std::size_t w = m_Reader.put(buf, ec); - ASSERT(!ec); - buf += w; - } - m_PendingBufferSize += length; + m_Message << std::string_view(s, length); } void MayFlush(boost::asio::yield_context& yield) override { - if (m_PendingBufferSize >= m_MinPendingBufferSize) { + if (m_Message.Size() >= m_MinPendingBufferSize) { m_Message.Flush(yield); - m_PendingBufferSize = 0; } } private: - HttpResponse::body_type::reader m_Reader; HttpResponse& m_Message; - // The size of the pending buffer to avoid unnecessary writes - std::size_t m_PendingBufferSize{0}; - // Minimum size of the pending buffer before we flush the data to the underlying stream + /** + * Minimum size of the pending buffer before we flush the data to the underlying stream. + * + * This magic number represents 4x the default size of the internal buffer used by @c AsioTlsStream, + * which is 1024 bytes. This ensures that we don't flush too often and allows for efficient streaming + * of the response body. The value is chosen to balance between performance and memory usage, as flushing + * too often can lead to excessive I/O operations and increased latency, while flushing too infrequently + * can lead to high memory usage. + */ static constexpr std::size_t m_MinPendingBufferSize = 4096; }; @@ -131,37 +117,108 @@ HttpResponse::HttpResponse(Shared::Ptr stream) { } +/** + * Get the size of the current response body buffer. + * + * @return size of the response buffer + */ +std::size_t HttpResponse::Size() const +{ + return m_Buffer.size(); +} + +/** + * Checks whether the HTTP response headers have been written to the stream. + * + * @return true if the HTTP response headers have been written to the stream, false otherwise. + */ +bool HttpResponse::IsHeaderDone() const +{ + return m_HeaderDone; +} + +/** + * Flush the response to the underlying stream. + * + * This function flushes the response body and headers to the underlying stream in the following two ways: + * - If the response is chunked, it writes the body in chunks using the @c boost::beast::http::make_chunk() function. + * The size of the current @c m_Buffer represents the chunk size with each call to this function. If there is no + * data in the buffer, and the response isn't finished yet, it's a no-op and will silently return. Otherwise, if + * the response is finished, it writes the last chunk with a size of zero to indicate the end of the response. + * + * - If the response is not chunked, it writes the entire buffer at once using the @c boost::asio::async_write() + * function. The content length is set to the size of the current @c m_Buffer. Calling this function more than + * once will immediately terminate the process in debug builds, as this should never happen and indicates a + * programming error. + * + * The headers are written in the same way in both cases, using the @c boost::beast::http::response_serializer<> class. + * + * @param yc The yield context for asynchronous operations + */ void HttpResponse::Flush(boost::asio::yield_context yc) { if (!chunked()) { - ASSERT(!m_Serializer.is_header_done()); - prepare_payload(); + ASSERT(!m_HeaderDone); + content_length(Size()); } - boost::system::error_code ec; - boost::beast::http::async_write(*m_Stream, m_Serializer, yc[ec]); - if (ec && ec != boost::beast::http::error::need_buffer) { - if (yc.ec_) { - *yc.ec_ = ec; - return; - } else { - BOOST_THROW_EXCEPTION(boost::system::system_error{ec}); + if (!m_HeaderDone) { + m_HeaderDone = true; + boost::beast::http::response_serializer s(*this); + boost::beast::http::async_write_header(*m_Stream, s, yc); + } + + if (chunked()) { + bool flush = false; + // In case this is our last chunk (i.e., the response is finished), we need to write any + // remaining data in the buffer before we terminate the response with a zero-sized last chunk. + if (Size() > 0) { + boost::system::error_code ec; + boost::asio::async_write(*m_Stream, boost::beast::http::make_chunk(m_Buffer.data()), yc[ec]); + if (ec) { + if (yc.ec_) { + *yc.ec_ = std::move(ec); + return; + } + BOOST_THROW_EXCEPTION(boost::system::system_error(ec, "Failed to write chunked response")); + } + + // The above write operation is guaranteed to either write the entire buffer or fail, and if it's the + // latter, we shouldn't reach this point, so we can safely trim the entire buffer input sequence here. + m_Buffer.consume(Size()); + flush = true; } + + if (m_Finished) { + // If the response is finished, we need to write the last chunk with a size of zero. + boost::asio::async_write(*m_Stream, boost::beast::http::make_chunk_last(), yc); + } else if (!flush) { + // If we haven't written any data, we don't need to trigger a flush operation as well. + return; + } + } else { + // Non-chunked response, write the entire buffer at once. + async_write(*m_Stream, m_Buffer, yc); + m_Buffer.consume(Size()); // Won't be reused, but we want to free the memory ASAP. } m_Stream->async_flush(yc); - - ASSERT(chunked() || m_Serializer.is_done()); } +/** + * Start streaming the response body in chunks. + * + * This function sets the response to use chunked encoding and prepares it for streaming. + * It must be called before any data is written to the response body, otherwise it will trigger an assertion failure. + * + * @note This function should only be called if the response body is expected to be streamed in chunks. + */ void HttpResponse::StartStreaming() { - ASSERT(body().Size() == 0 && !m_Serializer.is_header_done()); - body().Start(); + ASSERT(Size() == 0 && !m_HeaderDone); chunked(true); } JsonEncoder HttpResponse::GetJsonEncoder(bool pretty) { - auto adapter = std::make_shared(*this); - return JsonEncoder{adapter, pretty}; + return JsonEncoder{std::make_shared(*this), pretty}; } diff --git a/lib/remote/httpmessage.hpp b/lib/remote/httpmessage.hpp index bc67301c1..71606e2bd 100644 --- a/lib/remote/httpmessage.hpp +++ b/lib/remote/httpmessage.hpp @@ -9,198 +9,11 @@ #include "remote/url.hpp" #include "remote/apiuser.hpp" #include -#include +#include namespace icinga { -/** - * A custom body_type for a @c boost::beast::http::message - * - * It combines the memory management of @c boost::beast::http::dynamic_body, - * which uses a multi_buffer, with the ability to continue serialization when - * new data arrives of the @c boost::beast::http::buffer_body. - * - * @tparam DynamicBuffer A buffer conforming to the boost::beast interface of the same name - * - * @ingroup remote - */ -template -struct SerializableBody -{ - class reader; - class writer; - - class value_type - { - public: - template - value_type& operator<<(T && right) - { - /* Preferably, we would return an ostream object here instead. However - * there seems to be a bug in boost::beast where if the ostream, or rather its - * streambuf object is moved into the return value, the chunked encoding gets - * mangled, leading to the client disconnecting. - * - * A workaround would have been to construct the boost::beast::detail::ostream_helper - * with the last parameter set to false, indicating that the streambuf object is not - * movable, but that is an implementation detail we'd rather not use directly in our - * code. - * - * This version has a certain overhead of the ostream being constructed on every call - * to the operator, which leads to an individual append for each time, whereas if the - * object could be kept until the entire chain of output operators is finished, only - * a single call to prepare()/commit() would have been needed. - * - * However, since this operator is mostly used for small error messages and the big - * responses are handled via a reader instance, this shouldn't be too much of a - * problem. - */ - boost::beast::ostream(m_Buffer) << std::forward(right); - return *this; - } - - std::size_t Size() const { return m_Buffer.size(); } - - void Finish() { m_More = false; } - void Start() { m_More = true; } - - friend class reader; - friend class writer; - - private: - /* This defaults to false so the body does not require any special handling - * for simple messages and can still be written with http::async_write(). - */ - bool m_More = false; - DynamicBuffer m_Buffer; - }; - - static std::uint64_t size(const value_type& body) { return body.Size(); } - - /** - * Implement the boost::beast BodyReader interface for this body type - * - * This is used by the @c boost::beast::http::parser (which we don't use for responses) - * or in the @c BeastHttpMessageAdapter for our own @c JsonEncoder to write JSON directly - * into the body of a HTTP response message. - * - * The reader automatically sets the body's `more` flag on the call to its init() function - * and resets it when its finish() function is called. Regarding the usage in the - * @c JsonEncoder above, this means that the message is automatically marked as complete - * when the encoder object is destroyed. - */ - class reader - { - public: - template - explicit reader(boost::beast::http::header& h, value_type& b) : m_Body(b) - { - } - - void init(const boost::optional& n, boost::beast::error_code& ec) - { - ec = {}; - m_Body.Start(); -#if BOOST_VERSION >= 107000 - if (n) { - m_Body.m_Buffer.reserve(*n); - } -#endif /* BOOST_VERSION */ - } - - template - std::size_t put(const ConstBufferSequence& buffers, boost::beast::error_code& ec) - { - auto size = boost::asio::buffer_size(buffers); - if (size > m_Body.m_Buffer.max_size() - m_Body.Size()) { - ec = boost::beast::http::error::buffer_overflow; - return 0; - } - - auto const wBuf = m_Body.m_Buffer.prepare(size); - boost::asio::buffer_copy(wBuf, buffers); - m_Body.m_Buffer.commit(size); - return size; - } - - void finish(boost::beast::error_code& ec) - { - ec = {}; - m_Body.Finish(); - } - - private: - value_type& m_Body; - }; - - /** - * Implement the boost::beast BodyWriter interface for this body type - * - * This is used (for example) by the @c boost::beast::http::serializer to write out the - * message over the TLS stream. The logic is similar to the writer of the - * @c boost::beast::http::buffer_body. - * - * On the every call, it will free up the buffer range that has previously been written, - * then return a buffer containing data the has become available in the meantime. Otherwise, - * if there is more data expected in the future, for example because a corresponding reader - * has not yet finished filling the body, a `need_buffer` error is returned, to inform the - * serializer to abort writing for now, which in turn leads to the outer call to - * `http::async_write` to call their completion handlers with a `need_buffer` error, to - * notify that more data is required for another call to `http::async_write`. - */ - class writer - { - public: - using const_buffers_type = typename decltype(value_type::m_Buffer)::const_buffers_type; - -#if BOOST_VERSION > 106600 - template - explicit writer(const boost::beast::http::header& h, value_type& b) - : m_Body(b) - { - } -#else - /** - * This constructor is needed specifically for boost-1.66, which was the first version - * the beast library was introduced and is still used on older (supported) distros. - */ - template - explicit writer(const boost::beast::http::message& msg) - : m_Body(const_cast(msg.body())) - { - } -#endif - void init(boost::beast::error_code& ec) { ec = {}; } - - boost::optional> get(boost::beast::error_code& ec) - { - using namespace boost::beast::http; - - if (m_SizeWritten > 0) { - m_Body.m_Buffer.consume(std::exchange(m_SizeWritten, 0)); - } - - if (m_Body.m_Buffer.size()) { - ec = {}; - m_SizeWritten = m_Body.m_Buffer.size(); - return {{m_Body.m_Buffer.data(), m_Body.m_More}}; - } - - if (m_Body.m_More) { - ec = {make_error_code(error::need_buffer)}; - } else { - ec = {}; - } - return boost::none; - } - - private: - value_type& m_Body; - std::size_t m_SizeWritten = 0; - }; -}; - /** * A wrapper class for a boost::beast HTTP request * @@ -254,40 +67,74 @@ private: }; /** - * A wrapper class for a boost::beast HTTP response + * A wrapper class for a boost::beast HTTP response. + * + * This is a convenience class to handle HTTP responses in a more streaming-like way. This class is derived + * from @c boost::beast::http::response and as its name implies, it doesn't + * and won't contain a body. Instead, it uses its own internal buffer to efficiently manage the response body + * data, which can be filled using the output stream operator (`<<`) or the @c JsonEncoder class. This allows + * for a more flexible and efficient way to handle HTTP responses, without having to copy Asio/Beast buffers + * around. It uses a @c boost::asio::streambuf internally to manage the response body data, which is suitable + * for the @c JsonEncoder class to write JSON data directly into it and then forward it as-is to Asio's + * @c boost::asio::async_write() function. + * + * Furthermore, it supports chunked encoding as well as fixed-size responses. If chunked encoding is used, the + * @c m_Buffer is written in chunks created by the @c boost::beast::http::make_chunk() function, which doesn't + * copy the buffer data, but instead creates a view similar to @c std::string_view but for buffers and allows + * for efficient streaming of the response body data. Once you've finished writing to the response, you must + * call the @c Finish() followed by a call to @c Flush() to indicate that there will be no more data to stream + * and to write the final chunk with a size of zero to indicate the end of the response. If you don't call @c Finish(), + * the response will not be flushed and the client will not receive the final chunk, which may lead to a timeout + * or other issues on the client side. * * @ingroup remote */ -class HttpResponse: public boost::beast::http::response> +class HttpResponse : public boost::beast::http::response { public: explicit HttpResponse(Shared::Ptr stream); - /** - * Writes as much of the response as is currently available. - * - * Uses chunk-encoding if the content_length has not been set by the time this is called - * for the first time. - * - * The caller needs to ensure that the header is finished before calling this for the - * first time as changes to the header afterwards will not have any effect. - * - * @param yc The yield_context for this operation - */ + std::size_t Size() const; + bool IsHeaderDone() const; + void Flush(boost::asio::yield_context yc); - bool HasSerializationStarted () { return m_Serializer.is_header_done(); } - - /** - * Enables chunked encoding. - */ void StartStreaming(); + void Finish() + { + m_Finished = true; + } + JsonEncoder GetJsonEncoder(bool pretty = false); - + + /** + * Write data to the response body. + * + * This function appends the given data to the response body using the internal output stream. + * This is the only available way to fill the response body with data and must not be called + * after the response has been finished (i.e., after calling @c Finish()). + * + * @tparam T The type of the data to write. It can be any type that supports the output stream operator (`<<`). + * + * @param data The data to write to the response body + * + * @return Reference to this HttpResponse object for chaining operations. + */ + template + HttpResponse& operator<<(T&& data) + { + ASSERT(!m_Finished); // Never allow writing to an already finished response. + m_Ostream << std::forward(data); + return *this; + } + private: - using Serializer = boost::beast::http::response_serializer; - Serializer m_Serializer{*this}; + bool m_HeaderDone = false; // Indicates if the response headers have been written to the stream. + bool m_Finished = false; // Indicates if the response is finished and no more data will be written. + + boost::asio::streambuf m_Buffer; // The buffer used to store the response body. + std::ostream m_Ostream{&m_Buffer}; // The output stream used to write data to the response body. Shared::Ptr m_Stream; }; diff --git a/lib/remote/httpserverconnection.cpp b/lib/remote/httpserverconnection.cpp index 0345ab5f5..230b54c9d 100644 --- a/lib/remote/httpserverconnection.cpp +++ b/lib/remote/httpserverconnection.cpp @@ -148,7 +148,7 @@ bool EnsureValidHeaders( HttpUtility::SendJsonError(response, nullptr, 400, "Bad Request: " + errorMsg); } else { response.set(http::field::content_type, "text/html"); - response.body() << "

Bad Request

" << errorMsg << "

\r\n"; + response << "

Bad Request

" << errorMsg << "

\r\n"; } response.set(http::field::connection, "close"); @@ -207,7 +207,7 @@ bool HandleAccessControl( response.result(http::status::ok); response.set(http::field::access_control_allow_methods, "GET, POST, PUT, DELETE"); response.set(http::field::access_control_allow_headers, "Authorization, Content-Type, X-HTTP-Method-Override"); - response.body() << "Preflight OK"; + response << "Preflight OK"; response.set(http::field::connection, "close"); response.Flush(yc); @@ -233,7 +233,7 @@ bool EnsureAcceptHeader( if (request.method() != http::verb::get && request[http::field::accept] != "application/json") { response.result(http::status::bad_request); response.set(http::field::content_type, "text/html"); - response.body() << "

Accept header is missing or not set to 'application/json'.

\r\n"; + response << "

Accept header is missing or not set to 'application/json'.

\r\n"; response.set(http::field::connection, "close"); response.Flush(yc); @@ -265,7 +265,7 @@ bool EnsureAuthenticatedUser( HttpUtility::SendJsonError(response, nullptr, 401, "Unauthorized. Please check your user credentials."); } else { response.set(http::field::content_type, "text/html"); - response.body() << "

Unauthorized. Please check your user credentials.

\r\n"; + response << "

Unauthorized. Please check your user credentials.

\r\n"; } response.Flush(yc); @@ -346,7 +346,7 @@ bool EnsureValidBody( HttpUtility::SendJsonError(response, nullptr, 400, "Bad Request: " + ec.message()); } else { response.set(http::field::content_type, "text/html"); - response.body() << "

Bad Request

" << ec.message() << "

\r\n"; + response << "

Bad Request

" << ec.message() << "

\r\n"; } response.set(http::field::connection, "close"); @@ -379,7 +379,7 @@ bool ProcessRequest( /* Since we don't know the state the stream is in, we can't send an error response and * have to just cause a disconnect here. */ - if (response.HasSerializationStarted()) { + if (response.IsHeaderDone()) { throw; } @@ -388,7 +388,7 @@ bool ProcessRequest( return true; } - response.body().Finish(); + response.Finish(); response.Flush(yc); return true; diff --git a/lib/remote/infohandler.cpp b/lib/remote/infohandler.cpp index afc62f50b..94c044508 100644 --- a/lib/remote/infohandler.cpp +++ b/lib/remote/infohandler.cpp @@ -76,23 +76,22 @@ bool InfoHandler::HandleRequest( } else { response.set(http::field::content_type, "text/html"); - auto & body = response.body(); - body << "Icinga 2

Hello from Icinga 2 (Version: " + response << "Icinga 2

Hello from Icinga 2 (Version: " << Application::GetAppVersion() << ")!

" << "

You are authenticated as " << user->GetName() << ". "; if (!permInfo.empty()) { - body << "Your user has the following permissions:

    "; + response << "Your user has the following permissions:

      "; for (const String& perm : permInfo) { - body << "
    • " << perm << "
    • "; + response << "
    • " << perm << "
    • "; } - body << "
    "; + response << "
"; } else - body << "Your user does not have any permissions.

"; + response << "Your user does not have any permissions.

"; - body << R"(

More information about API requests is available in the documentation.

)"; + response << R"(

More information about API requests is available in the documentation.

)"; } return true; diff --git a/lib/remote/mallocinfohandler.cpp b/lib/remote/mallocinfohandler.cpp index 4ca37d555..1c32461ff 100644 --- a/lib/remote/mallocinfohandler.cpp +++ b/lib/remote/mallocinfohandler.cpp @@ -86,7 +86,7 @@ bool MallocInfoHandler::HandleRequest( response.result(200); response.set(http::field::content_type, "application/xml"); - response.body() << std::string_view(buf, bufSize); + response << std::string_view(buf, bufSize); #endif /* HAVE_MALLOC_INFO */ return true; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index fb94a4384..435e0a2ee 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -279,7 +279,6 @@ add_boost_test(base remote_httpmessage/request_parse remote_httpmessage/request_params remote_httpmessage/response_flush_nothrow - remote_httpmessage/response_body_reader remote_httpmessage/response_write_empty remote_httpmessage/response_write_fixed remote_httpmessage/response_write_chunked @@ -311,7 +310,6 @@ if(BUILD_TESTING) base-remote_httpmessage/request_parse base-remote_httpmessage/request_params base-remote_httpmessage/response_flush_nothrow - base-remote_httpmessage/response_body_reader base-remote_httpmessage/response_write_empty base-remote_httpmessage/response_write_fixed base-remote_httpmessage/response_write_chunked diff --git a/test/remote-httpmessage.cpp b/test/remote-httpmessage.cpp index c9375c80e..84b0fef1b 100644 --- a/test/remote-httpmessage.cpp +++ b/test/remote-httpmessage.cpp @@ -64,47 +64,6 @@ BOOST_AUTO_TEST_CASE(request_params) BOOST_REQUIRE(!HttpUtility::GetLastParameter(params, "test4")); } -BOOST_AUTO_TEST_CASE(response_body_reader) -{ - auto & io = IoEngine::Get(); - io.SpawnCoroutine(io.GetIoContext(), [&, this](boost::asio::yield_context yc) { - HttpResponse response(server); - response.result(http::status::ok); - response.StartStreaming(); - - SerializableBody::reader rd{response.base(), response.body()}; - boost::system::error_code ec; - rd.init(3, ec); - BOOST_REQUIRE(!ec); - - boost::asio::const_buffer seq1("test1", 5); - rd.put(seq1, ec); - BOOST_REQUIRE(!ec); - BOOST_REQUIRE_NO_THROW(response.Flush(yc)); - - boost::asio::const_buffer seq2("test2", 5); - rd.put(seq2, ec); - BOOST_REQUIRE(!ec); - BOOST_REQUIRE_NO_THROW(response.Flush(yc)); - - rd.finish(ec); - BOOST_REQUIRE(!ec); - BOOST_REQUIRE_NO_THROW(response.Flush(yc)); - - BOOST_REQUIRE(Shutdown(server, yc)); - }); - - http::response_parser parser; - flat_buffer buf; - http::read(*client, buf, parser); - - BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok); - BOOST_REQUIRE_EQUAL(parser.get().chunked(), true); - BOOST_REQUIRE_EQUAL(parser.get().body(), "test1test2"); - - BOOST_REQUIRE(Shutdown(client)); -} - BOOST_AUTO_TEST_CASE(response_flush_nothrow) { auto& io = IoEngine::Get(); @@ -157,7 +116,7 @@ BOOST_AUTO_TEST_CASE(response_write_fixed) io.SpawnCoroutine(io.GetIoContext(), [&, this](boost::asio::yield_context yc) { HttpResponse response(server); response.result(http::status::ok); - response.body() << "test"; + response << "test"; BOOST_REQUIRE_NO_THROW(response.Flush(yc)); @@ -183,14 +142,14 @@ BOOST_AUTO_TEST_CASE(response_write_chunked) response.result(http::status::ok); response.StartStreaming(); - response.body() << "test" << 1; + response << "test" << 1; BOOST_REQUIRE_NO_THROW(response.Flush(yc)); - response.body() << "test" << 2; + response << "test" << 2; BOOST_REQUIRE_NO_THROW(response.Flush(yc)); - response.body() << "test" << 3; - response.body().Finish(); + response << "test" << 3; + response.Finish(); BOOST_REQUIRE_NO_THROW(response.Flush(yc)); BOOST_REQUIRE(Shutdown(server, yc)); diff --git a/test/remote-httpserverconnection.cpp b/test/remote-httpserverconnection.cpp index 2ad1563f8..9a5597cb9 100644 --- a/test/remote-httpserverconnection.cpp +++ b/test/remote-httpserverconnection.cpp @@ -84,7 +84,7 @@ class UnitTestHandler final: public HttpHandler response.StartStreaming(); response.Flush(yc); for (;;) { - response.body() << "test"; + response << "test"; response.Flush(yc); } @@ -93,12 +93,12 @@ class UnitTestHandler final: public HttpHandler if (HttpUtility::GetLastParameter(request.Params(), "throw")) { response.StartStreaming(); - response.body() << "test"; + response << "test"; response.Flush(yc); throw std::runtime_error{"The front fell off"}; } - response.body() << "test"; + response << "test"; return true; } };