Send small static files with the headers and large bodies without a copy (#2589)

A file served from a mount point or through set_file_content() left in two
writes, one for the status line and headers and one for the body, because the
body came from a content provider. A small file is now read into the header
buffer so the whole response leaves in a single write. Only file-backed
providers are coalesced this way: a user-supplied provider may produce its data
over time, and holding the headers back until it finishes would stall the
client.

A large set_content() body was copied into the header buffer before being
sent. A body of CPPHTTPLIB_SEND_BUFSIZ or more is now written directly after
the headers, which saves the copy at the cost of one extra write.
This commit is contained in:
yhirose
2026-09-26 22:19:15 -04:00
committed by GitHub
parent 8b6ab24159
commit 2fb2dbbe1e
+41 -15
View File
@@ -1884,6 +1884,7 @@ struct Response {
ContentProvider content_provider_; ContentProvider content_provider_;
ContentProviderResourceReleaser content_provider_resource_releaser_; ContentProviderResourceReleaser content_provider_resource_releaser_;
bool is_chunked_content_provider_ = false; bool is_chunked_content_provider_ = false;
bool is_file_content_provider_ = false;
bool content_provider_success_ = false; bool content_provider_success_ = false;
std::string file_content_path_; std::string file_content_path_;
std::string file_content_content_type_; std::string file_content_content_type_;
@@ -8788,9 +8789,9 @@ inline bool compress_content_provider(const ContentProvider &content_provider,
return cmp.compress(nullptr, 0, true, append); return cmp.compress(nullptr, 0, true, append);
} }
// Serves `m` as the response body. `set_content_provider()` clears the coding, // Serves `m` as the response body. `set_content_provider()` clears the coding
// so recording it has to come after; keeping both here means a third // and the file flag, so recording them has to come after; keeping all of it
// file-serving path cannot get that order wrong. // here means a third file-serving path cannot get that order wrong.
inline void set_file_content_provider(Response &res, inline void set_file_content_provider(Response &res,
const std::shared_ptr<mmap> &m, const std::shared_ptr<mmap> &m,
const std::string &content_type, const std::string &content_type,
@@ -8802,6 +8803,7 @@ inline void set_file_content_provider(Response &res,
return true; return true;
}); });
res.is_file_content_provider_ = true;
res.content_coding_ = encoding; res.content_coding_ = encoding;
} }
@@ -11702,6 +11704,7 @@ inline void Response::set_content_provider(
if (in_length > 0) { content_provider_ = std::move(provider); } if (in_length > 0) { content_provider_ = std::move(provider); }
content_provider_resource_releaser_ = std::move(resource_releaser); content_provider_resource_releaser_ = std::move(resource_releaser);
is_chunked_content_provider_ = false; is_chunked_content_provider_ = false;
is_file_content_provider_ = false;
content_coding_ = detail::EncodingType::None; content_coding_ = detail::EncodingType::None;
} }
@@ -11713,6 +11716,7 @@ inline void Response::set_content_provider(
content_provider_ = detail::ContentProviderAdapter(std::move(provider)); content_provider_ = detail::ContentProviderAdapter(std::move(provider));
content_provider_resource_releaser_ = std::move(resource_releaser); content_provider_resource_releaser_ = std::move(resource_releaser);
is_chunked_content_provider_ = false; is_chunked_content_provider_ = false;
is_file_content_provider_ = false;
content_coding_ = detail::EncodingType::None; content_coding_ = detail::EncodingType::None;
} }
@@ -11724,6 +11728,7 @@ inline void Response::set_chunked_content_provider(
content_provider_ = detail::ContentProviderAdapter(std::move(provider)); content_provider_ = detail::ContentProviderAdapter(std::move(provider));
content_provider_resource_releaser_ = std::move(resource_releaser); content_provider_resource_releaser_ = std::move(resource_releaser);
is_chunked_content_provider_ = true; is_chunked_content_provider_ = true;
is_file_content_provider_ = false;
content_coding_ = detail::EncodingType::None; content_coding_ = detail::EncodingType::None;
} }
@@ -13377,9 +13382,27 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
if (!detail::write_response_line(bstrm, res.status)) { return false; } if (!detail::write_response_line(bstrm, res.status)) { return false; }
if (header_writer_(bstrm, res.headers) <= 0) { return false; } if (header_writer_(bstrm, res.headers) <= 0) { return false; }
// Combine small body with headers to reduce write syscalls // Combine a small body with the headers so the whole response leaves in a
if (req.method != "HEAD" && !res.body.empty() && !res.content_provider_) { // single write. A large body is written on its own instead: a copy of it
bstrm.write(res.body.data(), res.body.size()); // costs more than the extra write saves.
auto send_body = req.method != "HEAD";
auto body_is_separate = false;
auto provider_done = false;
if (send_body && !res.body.empty() && !res.content_provider_) {
if (res.body.size() < CPPHTTPLIB_SEND_BUFSIZ) {
bstrm.write(res.body.data(), res.body.size());
} else {
body_is_separate = true;
}
} else if (send_body && res.content_provider_ &&
res.is_file_content_provider_ &&
res.content_length_ < CPPHTTPLIB_SEND_BUFSIZ) {
// A small file is read into the same buffer. Other providers may produce
// their data over time, so they are never held back.
if (!write_content_with_provider(bstrm, req, res, boundary, content_type)) {
return false;
}
provider_done = true;
} }
// Log before writing to avoid race condition with client-side code that // Log before writing to avoid race condition with client-side code that
@@ -13390,17 +13413,20 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
auto &data = bstrm.get_buffer(); auto &data = bstrm.get_buffer();
if (!detail::write_data(strm, data.data(), data.size())) { return false; } if (!detail::write_data(strm, data.data(), data.size())) { return false; }
// Streaming body if (body_is_separate) {
auto ret = true; return detail::write_data(strm, res.body.data(), res.body.size());
if (req.method != "HEAD" && res.content_provider_) {
if (write_content_with_provider(strm, req, res, boundary, content_type)) {
res.content_provider_success_ = true;
} else {
ret = false;
}
} }
return ret; // Streaming body
if (send_body && res.content_provider_) {
if (!provider_done &&
!write_content_with_provider(strm, req, res, boundary, content_type)) {
return false;
}
res.content_provider_success_ = true;
}
return true;
} }
inline bool inline bool