mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-19 11:19:36 +00:00
Frame response bodies in one place
A content provider that wrote past the length it was given put the extra bytes after the framed body, where a keep-alive peer read them as the next message. Write only what fits and fail the write, on both the server and the client. Content-Length and Transfer-Encoding a handler set were sent next to the ones the library worked out, for ranges and compression, and a post-routing handler could change the body after Content-Length was set. Work both out in one place, from the body about to be written, again after the post-routing handler; a HEAD response keeps the length its handler gave it. A 1xx, 204 or 304 response sends no body and, following RFC 9110 8.6, no Content-Length. The client still sends a Content-Length or Transfer-Encoding its caller passes, as curl and urllib3 do, and the README says these have to match the body.
This commit is contained in:
1 parent
bc837b87fc
commit
43c9c6ddf3
3 files changed
+289
-49
No files matched your search
@@ -8857,6 +8857,12 @@ inline bool write_content_with_progress(Stream &strm,
|
||||
|
||||
data_sink.write = [&](const char *d, size_t l) -> bool {
|
||||
if (ok) {
|
||||
// Bytes past `length` would follow the framed body, where the peer reads
|
||||
// them as the next message. Write only what fits and fail the rest.
|
||||
if (l > end_offset - offset) {
|
||||
l = end_offset - offset;
|
||||
ok = false;
|
||||
}
|
||||
if (write_data(strm, d, l)) {
|
||||
offset += l;
|
||||
|
||||
@@ -12060,6 +12066,58 @@ inline void prefer_assigned_body(Response &res) {
|
||||
}
|
||||
}
|
||||
|
||||
// RFC 9112 6.3: a 1xx, 204 or 304 response ends with its header section.
|
||||
inline bool body_allowed_for_status(int status) {
|
||||
return status >= 200 && status != StatusCode::NoContent_204 &&
|
||||
status != StatusCode::NotModified_304;
|
||||
}
|
||||
|
||||
// Only a 206 response is served as a partial representation. range_error()
|
||||
// validates `req.ranges` only for a 2xx status, so slicing under any other
|
||||
// status would start from an unchecked offset.
|
||||
inline bool is_partial_content(const Request &req, const Response &res) {
|
||||
return !req.ranges.empty() && res.status == StatusCode::PartialContent_206;
|
||||
}
|
||||
|
||||
// Sets the Content-Length or Transfer-Encoding framing the body that
|
||||
// Server::write_response_core() writes. Only the library sets them: one from a
|
||||
// handler would go out next to it, or describe some other body.
|
||||
inline void set_framing_headers(const Request &req, Response &res,
|
||||
const std::string &boundary,
|
||||
const std::string &content_type) {
|
||||
// A HEAD response may give the length a GET would have had (RFC 9110 9.3.2).
|
||||
auto head_length = req.method == "HEAD"
|
||||
? res.get_header_value("Content-Length")
|
||||
: std::string();
|
||||
res.headers.erase("Content-Length");
|
||||
res.headers.erase("Transfer-Encoding");
|
||||
|
||||
if (!body_allowed_for_status(res.status)) { return; }
|
||||
|
||||
if (res.content_provider_) {
|
||||
if (res.content_length_ == 0) {
|
||||
if (res.is_chunked_content_provider_) {
|
||||
res.set_header("Transfer-Encoding", "chunked");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// The part Server::write_content_with_provider() writes.
|
||||
auto length = res.content_length_;
|
||||
if (is_partial_content(req, res)) {
|
||||
length = req.ranges.size() == 1
|
||||
? get_range_offset_and_length(req.ranges[0], length).second
|
||||
: get_multipart_ranges_data_length(req, boundary,
|
||||
content_type, length);
|
||||
}
|
||||
res.set_header("Content-Length", std::to_string(length));
|
||||
} else if (!res.body.empty()) {
|
||||
res.set_header("Content-Length", std::to_string(res.body.size()));
|
||||
} else {
|
||||
res.set_header("Content-Length", head_length.empty() ? "0" : head_length);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
inline size_t Response::get_header_value_u64(const std::string &key, size_t def,
|
||||
@@ -13777,6 +13835,9 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
|
||||
need_apply_ranges = true;
|
||||
}
|
||||
|
||||
if (!detail::body_allowed_for_status(res.status)) {
|
||||
detail::clear_content(res);
|
||||
}
|
||||
detail::prefer_assigned_body(res);
|
||||
|
||||
std::string content_type;
|
||||
@@ -13807,16 +13868,17 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
|
||||
res.set_header("Content-Type", "text/plain");
|
||||
}
|
||||
|
||||
if (res.body.empty() && !res.content_length_ && !res.content_provider_ &&
|
||||
!res.has_header("Content-Length")) {
|
||||
res.set_header("Content-Length", "0");
|
||||
}
|
||||
detail::set_framing_headers(req, res, boundary, content_type);
|
||||
|
||||
if (req.method == "HEAD" && !res.has_header("Accept-Ranges")) {
|
||||
res.set_header("Accept-Ranges", "bytes");
|
||||
}
|
||||
|
||||
if (post_routing_handler_) { post_routing_handler_(req, res); }
|
||||
if (post_routing_handler_) {
|
||||
post_routing_handler_(req, res);
|
||||
// Again, for a body or framing headers the handler changed.
|
||||
detail::set_framing_headers(req, res, boundary, content_type);
|
||||
}
|
||||
|
||||
// Response line and headers
|
||||
detail::BufferStream bstrm;
|
||||
@@ -13826,7 +13888,8 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
|
||||
// Combine a small body with the headers so the whole response leaves in a
|
||||
// single write. A large body is written on its own instead: a copy of it
|
||||
// costs more than the extra write saves.
|
||||
auto send_body = req.method != "HEAD";
|
||||
auto send_body =
|
||||
req.method != "HEAD" && detail::body_allowed_for_status(res.status);
|
||||
auto body_is_separate = false;
|
||||
auto provider_done = false;
|
||||
if (send_body && !res.body.empty() && !res.content_provider_) {
|
||||
@@ -13879,15 +13942,7 @@ Server::write_content_with_provider(Stream &strm, const Request &req,
|
||||
};
|
||||
|
||||
if (res.content_length_ > 0) {
|
||||
// Only a 206 response is served as a partial representation, matching the
|
||||
// condition `apply_ranges()` used to decide the Content-Length and the
|
||||
// multipart boundary. Since `detail::range_error()` validates `req.ranges`
|
||||
// only for a 2xx status, slicing under any other status would write a body
|
||||
// that disagrees with the header already sent, from an unchecked offset.
|
||||
auto is_partial =
|
||||
!req.ranges.empty() && res.status == StatusCode::PartialContent_206;
|
||||
|
||||
if (!is_partial) {
|
||||
if (!detail::is_partial_content(req, res)) {
|
||||
return detail::write_content(strm, res.content_provider_, 0,
|
||||
res.content_length_, is_shutting_down);
|
||||
} else if (req.ranges.size() == 1) {
|
||||
@@ -14588,8 +14643,8 @@ Server::static_file_encoding(const Request &req, const Response &res,
|
||||
return detail::encoding_type(req, res, content_type);
|
||||
}
|
||||
|
||||
// Compresses a file-backed content provider into `res.body` and takes over the
|
||||
// framing headers. Returns false when the response is left untouched.
|
||||
// Compresses a file-backed content provider into `res.body`. Returns false when
|
||||
// the response is left untouched.
|
||||
inline bool Server::apply_static_file_compression(const Request &req,
|
||||
Response &res) const {
|
||||
auto type = res.content_coding_;
|
||||
@@ -14617,7 +14672,6 @@ inline bool Server::apply_static_file_compression(const Request &req,
|
||||
|
||||
res.set_header("Content-Encoding", detail::encoding_name(type));
|
||||
res.set_header("Vary", "Accept-Encoding");
|
||||
res.set_header("Content-Length", std::to_string(res.body.size()));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -14631,7 +14685,7 @@ inline void Server::apply_ranges(const Request &req, Response &res,
|
||||
// ordinary body: `Content-Length` and HEAD keep working, and the response
|
||||
// takes the same path as `set_content()` from here on. Range requests never
|
||||
// get a content coding, so `Content-Range` still names identity bytes and
|
||||
// none of the framing below applies.
|
||||
// none of the range handling below applies.
|
||||
if (apply_static_file_compression(req, res)) { return; }
|
||||
|
||||
if (req.ranges.size() > 1 && res.status == StatusCode::PartialContent_206) {
|
||||
@@ -14650,34 +14704,19 @@ inline void Server::apply_ranges(const Request &req, Response &res,
|
||||
auto type = detail::encoding_type(req, res);
|
||||
|
||||
if (res.body.empty()) {
|
||||
if (res.content_length_ > 0) {
|
||||
size_t length = 0;
|
||||
if (req.ranges.empty() || res.status != StatusCode::PartialContent_206) {
|
||||
length = res.content_length_;
|
||||
} else if (req.ranges.size() == 1) {
|
||||
auto offset_and_length = detail::get_range_offset_and_length(
|
||||
req.ranges[0], res.content_length_);
|
||||
if (res.content_length_ > 0 && req.ranges.size() == 1 &&
|
||||
res.status == StatusCode::PartialContent_206) {
|
||||
auto offset_and_length = detail::get_range_offset_and_length(
|
||||
req.ranges[0], res.content_length_);
|
||||
|
||||
length = offset_and_length.second;
|
||||
|
||||
auto content_range = detail::make_content_range_header_field(
|
||||
offset_and_length, res.content_length_);
|
||||
res.set_header("Content-Range", content_range);
|
||||
} else {
|
||||
length = detail::get_multipart_ranges_data_length(
|
||||
req, boundary, content_type, res.content_length_);
|
||||
}
|
||||
res.set_header("Content-Length", std::to_string(length));
|
||||
} else {
|
||||
if (res.content_provider_) {
|
||||
if (res.is_chunked_content_provider_) {
|
||||
res.set_header("Transfer-Encoding", "chunked");
|
||||
res.content_coding_ = type;
|
||||
if (type != detail::EncodingType::None) {
|
||||
res.set_header("Content-Encoding", detail::encoding_name(type));
|
||||
res.set_header("Vary", "Accept-Encoding");
|
||||
}
|
||||
}
|
||||
auto content_range = detail::make_content_range_header_field(
|
||||
offset_and_length, res.content_length_);
|
||||
res.set_header("Content-Range", content_range);
|
||||
} else if (res.is_chunked_content_provider_) {
|
||||
res.content_coding_ = type;
|
||||
if (type != detail::EncodingType::None) {
|
||||
res.set_header("Content-Encoding", detail::encoding_name(type));
|
||||
res.set_header("Vary", "Accept-Encoding");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -14720,7 +14759,6 @@ inline void Server::apply_ranges(const Request &req, Response &res,
|
||||
}
|
||||
|
||||
res.content_length_ = res.body.size();
|
||||
res.set_header("Content-Length", std::to_string(res.content_length_));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user