diff options
author | Ed Tanous <ed@tanous.net> | 2023-08-07 22:02:40 +0300 |
---|---|---|
committer | Ed Tanous <ed@tanous.net> | 2023-10-31 20:43:04 +0300 |
commit | 27b0cf90f6cba207837f5c263a45c6ea5651975b (patch) | |
tree | 314cda7432edfe44fdd1297664db1399a1d6ba22 /http/http_response.hpp | |
parent | 522377dcb85082da598403e104a44d621b4c2bb4 (diff) | |
download | bmcweb-27b0cf90f6cba207837f5c263a45c6ea5651975b.tar.xz |
Move to file_body in boost
As is, it reads the whole file into memory before sending it. While
fairly fast for the user, this wastes ram, and makes bmcweb less useful
on less capable systems.
This patch enables using the boost::beast::http::file_body type, which
has more efficient serialization semantics than using a std::string. To
do this, it adds a openFile() handler to http::Response, which can be
used to properly open a file. Once the file is opened, the existing
string body is ignored, and the file payload is sent instead.
openFile() also returns success or failure, to allow users to properly
handle 404s and other errors.
To prove that it works, I moved over every instance of direct use of the
body() method over to using this, including the webasset handler. The
webasset handler specifically should help with system load when doing an
initial page load of the webui.
Tested:
Redfish service validator passes.
Change-Id: Ic7ea9ffefdbc81eb985de7edc0fac114822994ad
Signed-off-by: Ed Tanous <ed@tanous.net>
Diffstat (limited to 'http/http_response.hpp')
-rw-r--r-- | http/http_response.hpp | 202 |
1 files changed, 143 insertions, 59 deletions
diff --git a/http/http_response.hpp b/http/http_response.hpp index cb07a83635..a5f95a90b2 100644 --- a/http/http_response.hpp +++ b/http/http_response.hpp @@ -2,8 +2,11 @@ #include "logging.hpp" #include "utils/hex_utils.hpp" +#include <boost/beast/http/file_body.hpp> #include <boost/beast/http/message.hpp> +#include <boost/beast/http/message_generator.hpp> #include <boost/beast/http/string_body.hpp> +#include <boost/variant2/variant.hpp> #include <nlohmann/json.hpp> #include <optional> @@ -16,37 +19,52 @@ namespace crow template <typename Adaptor, typename Handler> class Connection; +namespace http = boost::beast::http; + struct Response { template <typename Adaptor, typename Handler> friend class crow::Connection; - using response_type = - boost::beast::http::response<boost::beast::http::string_body>; - response_type stringResponse; + using string_response = http::response<http::string_body>; + using file_response = http::response<http::file_body>; + + // Use boost variant2 because it doesn't have valueless by exception + boost::variant2::variant<string_response, file_response> response; nlohmann::json jsonValue; + using fields_type = http::header<false, http::fields>; + fields_type& fields() + { + return boost::variant2::visit( + [](auto&& r) -> fields_type& { return r.base(); }, response); + } - void addHeader(std::string_view key, std::string_view value) + const fields_type& fields() const { - stringResponse.insert(key, value); + return boost::variant2::visit( + [](auto&& r) -> const fields_type& { return r.base(); }, response); } - void addHeader(boost::beast::http::field key, std::string_view value) + void addHeader(std::string_view key, std::string_view value) { - stringResponse.insert(key, value); + fields().insert(key, value); } - void clearHeader(boost::beast::http::field key) + void addHeader(http::field key, std::string_view value) { - stringResponse.erase(key); + fields().insert(key, value); } - Response() = default; + void clearHeader(http::field key) + { + fields().erase(key); + } + Response() : response(string_response()) {} Response(Response&& res) noexcept : - stringResponse(std::move(res.stringResponse)), - jsonValue(std::move(res.jsonValue)), completed(res.completed) + response(std::move(res.response)), jsonValue(std::move(res.jsonValue)), + completed(res.completed) { // See note in operator= move handler for why this is needed. if (!res.completed) @@ -61,7 +79,6 @@ struct Response ~Response() = default; Response(const Response&) = delete; - Response& operator=(const Response& r) = delete; Response& operator=(Response&& r) noexcept @@ -72,8 +89,7 @@ struct Response { return *this; } - stringResponse = std::move(r.stringResponse); - r.stringResponse.clear(); + response = std::move(r.response); jsonValue = std::move(r.jsonValue); // Only need to move completion handler if not already completed @@ -98,27 +114,45 @@ struct Response void result(unsigned v) { - stringResponse.result(v); + fields().result(v); } - void result(boost::beast::http::status v) + void result(http::status v) { - stringResponse.result(v); + fields().result(v); } - boost::beast::http::status result() const + void copyBody(const Response& res) { - return stringResponse.result(); + const string_response* s = + boost::variant2::get_if<string_response>(&(res.response)); + if (s == nullptr) + { + BMCWEB_LOG_ERROR("Unable to copy a file"); + return; + } + string_response* myString = + boost::variant2::get_if<string_response>(&response); + if (myString == nullptr) + { + myString = &response.emplace<string_response>(); + } + myString->body() = s->body(); + } + + http::status result() const + { + return fields().result(); } unsigned resultInt() const { - return stringResponse.result_int(); + return fields().result_int(); } std::string_view reason() const { - return stringResponse.reason(); + return fields().reason(); } bool isCompleted() const noexcept @@ -126,75 +160,87 @@ struct Response return completed; } - std::string& body() + const std::string* body() { - return stringResponse.body(); + string_response* body = + boost::variant2::get_if<string_response>(&response); + if (body == nullptr) + { + return nullptr; + } + return &body->body(); } std::string_view getHeaderValue(std::string_view key) const { - return stringResponse.base()[key]; + return fields()[key]; } void keepAlive(bool k) { - stringResponse.keep_alive(k); + return boost::variant2::visit([k](auto&& r) { r.keep_alive(k); }, + response); } bool keepAlive() const { - return stringResponse.keep_alive(); + return boost::variant2::visit([](auto&& r) { return r.keep_alive(); }, + response); } - void preparePayload() + uint64_t getContentLength(boost::optional<uint64_t> pSize) { // This code is a throw-free equivalent to // beast::http::message::prepare_payload - boost::optional<uint64_t> pSize = stringResponse.payload_size(); - using boost::beast::http::status; - using boost::beast::http::status_class; - using boost::beast::http::to_status_class; + using http::status; + using http::status_class; + using http::to_status_class; if (!pSize) { - pSize = 0; + return 0; } - else + bool is1XXReturn = to_status_class(result()) == + status_class::informational; + if (*pSize > 0 && (is1XXReturn || result() == status::no_content || + result() == status::not_modified)) { - bool is1XXReturn = to_status_class(stringResponse.result()) == - status_class::informational; - if (*pSize > 0 && - (is1XXReturn || stringResponse.result() == status::no_content || - stringResponse.result() == status::not_modified)) - { - BMCWEB_LOG_CRITICAL( - "{} Response content provided but code was no-content or not_modified, which aren't allowed to have a body", - logPtr(this)); - pSize = 0; - body().clear(); - } + BMCWEB_LOG_CRITICAL("{} Response content provided but code was " + "no-content or not_modified, which aren't " + "allowed to have a body", + logPtr(this)); + return 0; } - stringResponse.content_length(*pSize); + return *pSize; + } + + uint64_t size() + { + return boost::variant2::visit( + [](auto&& res) -> uint64_t { return res.body().size(); }, response); + } + + void preparePayload() + { + boost::variant2::visit( + [this](auto&& r) { + r.content_length(getContentLength(r.payload_size())); + }, + response); } void clear() { BMCWEB_LOG_DEBUG("{} Clearing response containers", logPtr(this)); - stringResponse.clear(); - stringResponse.body().shrink_to_fit(); + response.emplace<string_response>(); jsonValue = nullptr; completed = false; expectedHash = std::nullopt; } - void write(std::string_view bodyPart) - { - stringResponse.body() += std::string(bodyPart); - } - std::string computeEtag() const { // Only set etag if this request succeeded - if (result() != boost::beast::http::status::ok) + if (result() != http::status::ok) { return ""; } @@ -207,12 +253,24 @@ struct Response return "\"" + intToHexString(hashval, 8) + "\""; } + void write(std::string&& bodyPart) + { + string_response* str = + boost::variant2::get_if<string_response>(&response); + if (str != nullptr) + { + str->body() += bodyPart; + return; + } + response.emplace<string_response>(result(), 11, std::move(bodyPart)); + } + void end() { std::string etag = computeEtag(); if (!etag.empty()) { - addHeader(boost::beast::http::field::etag, etag); + addHeader(http::field::etag, etag); } if (completed) { @@ -268,17 +326,17 @@ struct Response void setHashAndHandleNotModified() { // Can only hash if we have content that's valid - if (jsonValue.empty() || result() != boost::beast::http::status::ok) + if (jsonValue.empty() || result() != http::status::ok) { return; } size_t hashval = std::hash<nlohmann::json>{}(jsonValue); std::string hexVal = "\"" + intToHexString(hashval, 8) + "\""; - addHeader(boost::beast::http::field::etag, hexVal); + addHeader(http::field::etag, hexVal); if (expectedHash && hexVal == *expectedHash) { jsonValue = nullptr; - result(boost::beast::http::status::not_modified); + result(http::status::not_modified); } } @@ -287,6 +345,32 @@ struct Response expectedHash = hash; } + using message_generator = http::message_generator; + message_generator generator() + { + return boost::variant2::visit( + [](auto& r) -> message_generator { return std::move(r); }, + response); + } + + bool openFile(const std::filesystem::path& path) + { + http::file_body::value_type file; + boost::beast::error_code ec; + file.open(path.c_str(), boost::beast::file_mode::read, ec); + if (ec) + { + return false; + } + // store the headers on stack temporarily so we can reconstruct the new + // base with the old headers copied in. + http::header<false> headTemp = std::move(fields()); + file_response& fileResponse = + response.emplace<file_response>(std::move(headTemp)); + fileResponse.body() = std::move(file); + return true; + } + private: std::optional<std::string> expectedHash; bool completed = false; |