diff --git a/include/bitcoin/server/protocols/protocol_http.hpp b/include/bitcoin/server/protocols/protocol_http.hpp index 3fa46ded..ccf8202e 100644 --- a/include/bitcoin/server/protocols/protocol_http.hpp +++ b/include/bitcoin/server/protocols/protocol_http.hpp @@ -57,6 +57,16 @@ class BCS_API protocol_http /// Obtain cached request and clear cache (requires strand). network::http::request_cptr reset_request() NOEXCEPT; + /// Serialize the model into text, reserved from size_hint. The text is + /// the body and its length is the content_length, so the model is + /// serialized once and released before the write. A sufficient hint + /// serializes without reallocation, an insufficient one is correct but + /// grows, so the hint sizes the response rather than a reused buffer. + /// False where the model cannot be serialized, in which case text is not + /// assigned. + static bool materialize(std::string& text, + const boost::json::value& model, size_t size_hint) NOEXCEPT; + private: // This is protected by strand. network::http::request_cptr request_{}; diff --git a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp index 02105c32..f2900187 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp @@ -624,11 +624,17 @@ void protocol_bitcoind_rest::send_json(value&& model, add_common_headers(message, *request); add_access_control_headers(message, *request); message.set(field::content_type, json); - message.body() = json_value + + // bitcoind frames every response with a content_length, so the body is + // serialized here and its length is the length of the buffer written. + std::string text{}; + if (!materialize(text, model, size_hint)) { - .model = std::move(model), - .size_hint = size_hint - }; + send_internal_server_error(network::error::bad_alloc); + return; + } + + message.body() = std::move(text); message.prepare_payload(); SEND(std::move(message), handle_complete, _1, error::success); } diff --git a/src/protocols/protocol_html.cpp b/src/protocols/protocol_html.cpp index f0c2a68e..1f21cbed 100644 --- a/src/protocols/protocol_html.cpp +++ b/src/protocols/protocol_html.cpp @@ -189,7 +189,16 @@ void protocol_html::send_json(boost::json::value&& model, size_t size_hint, .model = std::move(model), .size_hint = size_hint }; - response.prepare_payload(); + + // http::body defines size(), so preparing the payload would declare a + // content_length. These clients read chunked and do not require one, and + // measuring a json body for it is a second serialization, so the framing + // is set here instead. A 1.0 response cannot chunk, so it is measured. + if (response.version() == version_1_1) + response.chunked(true); + else + response.prepare_payload(); + SEND(std::move(response), handle_complete, _1, error::success); } diff --git a/src/protocols/protocol_http.cpp b/src/protocols/protocol_http.cpp index d0a561dd..a5231124 100644 --- a/src/protocols/protocol_http.cpp +++ b/src/protocols/protocol_http.cpp @@ -18,6 +18,7 @@ */ #include +#include #include namespace libbitcoin { @@ -32,6 +33,51 @@ using namespace std::placeholders; BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) +// Serialization. +// ---------------------------------------------------------------------------- + +// The measure of an unmaterialized json body is a second serialization, as +// beast requires the length before the body is written. Serializing once into +// the buffer that is written obtains the same length from the bytes that carry +// it, and releases the model before the write. +bool protocol_http::materialize(std::string& text, + const boost::json::value& model, size_t size_hint) NOEXCEPT +{ + // The hint reserves the buffer, so a sufficient hint does not reallocate. + // The scratch is a serialization window, not a bound on the body. + constexpr size_t window = 4096; + std::string out{}; + + try + { + // Reserved within the guard, as a hint that exceeds max_size or + // cannot be allocated throws, which must not escape. + out.reserve(size_hint); + + std::array scratch{}; + boost::json::serializer serializer{ model.storage() }; + serializer.reset(&model); + + while (!serializer.done()) + { + const auto view = serializer.read(scratch.data(), scratch.size()); + + // No progress (edge case), as guarded by the json body writer. + if (view.empty()) + return false; + + out.append(view.data(), view.size()); + } + } + catch (...) + { + return false; + } + + text = std::move(out); + return true; +} + // Websocket dispatch. // ----------------------------------------------------------------------------