Skip to content

Commit 71f784b

Browse files
committed
Read incrementally to enforce max_response_body_size_
1 parent 337633e commit 71f784b

File tree

2 files changed

+166
-53
lines changed

2 files changed

+166
-53
lines changed

include/glaze/net/http_client.hpp

Lines changed: 64 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1710,7 +1710,7 @@ namespace glz
17101710

17111711
if (is_chunked) {
17121712
// Read chunked transfer-encoded body
1713-
for (;;) {
1713+
while (true) {
17141714
// Read chunk size line (hex digits followed by CRLF)
17151715
asio::error_code read_ec;
17161716
std::visit([&](auto& sock) { asio::read_until(*sock, response_buffer, "\r\n", read_ec); },
@@ -1747,7 +1747,7 @@ namespace glz
17471747

17481748
if (chunk_size == 0) {
17491749
// Terminal chunk; skip optional trailers and final CRLF (RFC 7230 §4.1)
1750-
for (;;) {
1750+
while (true) {
17511751
std::visit([&](auto& sock) { asio::read_until(*sock, response_buffer, "\r\n", read_ec); },
17521752
socket_var);
17531753
if (read_ec) {
@@ -1795,20 +1795,24 @@ namespace glz
17951795
}
17961796
else if (!has_content_length) {
17971797
// No Content-Length and not chunked: read until connection close (RFC 7230 §3.3.3)
1798+
// Read incrementally to enforce max_response_body_size_ without unbounded allocation
17981799
asio::error_code read_ec;
1799-
std::visit(
1800-
[&](auto& sock) {
1801-
asio::read(*sock, response_buffer, asio::transfer_all(), read_ec);
1802-
},
1803-
socket_var);
1804-
if (read_ec && read_ec != asio::error::eof) {
1805-
detail::close_socket(socket_var, connection_pool->graceful_ssl_shutdown());
1806-
return std::unexpected(read_ec);
1800+
while (true) {
1801+
std::visit(
1802+
[&](auto& sock) {
1803+
asio::read(*sock, response_buffer, asio::transfer_at_least(1), read_ec);
1804+
},
1805+
socket_var);
1806+
if (read_ec) break;
1807+
if (max_response_body_size_ > 0 && response_buffer.size() > max_response_body_size_) {
1808+
detail::close_socket(socket_var, connection_pool->graceful_ssl_shutdown());
1809+
return std::unexpected(make_error_code(http_client_error::response_too_large));
1810+
}
18071811
}
18081812

1809-
if (max_response_body_size_ > 0 && response_buffer.size() > max_response_body_size_) {
1813+
if (read_ec != asio::error::eof) {
18101814
detail::close_socket(socket_var, connection_pool->graceful_ssl_shutdown());
1811-
return std::unexpected(make_error_code(http_client_error::response_too_large));
1815+
return std::unexpected(read_ec);
18121816
}
18131817

18141818
response_body.assign(static_cast<const char*>(response_buffer.data().data()),
@@ -2206,6 +2210,52 @@ namespace glz
22062210
*socket_var);
22072211
}
22082212

2213+
// Async EOF-delimited body reading: reads incrementally until connection close,
2214+
// checking max_response_body_size_ after each read to prevent unbounded allocation.
2215+
template <typename CompletionHandler>
2216+
void async_read_eof_body(std::shared_ptr<socket_variant> socket_var, std::shared_ptr<asio::streambuf> buffer,
2217+
const url_parts& url, bool use_https, int status_code,
2218+
std::unordered_map<std::string, std::string> response_headers,
2219+
CompletionHandler&& handler)
2220+
{
2221+
std::visit(
2222+
[&, this](auto& sock) {
2223+
asio::async_read(
2224+
*sock, *buffer, asio::transfer_at_least(1),
2225+
[this, socket_var, buffer, url, use_https, status_code,
2226+
response_headers = std::move(response_headers),
2227+
handler = std::forward<CompletionHandler>(handler)](asio::error_code ec, std::size_t) mutable {
2228+
if (ec && ec != asio::error::eof) {
2229+
handler(std::unexpected(ec));
2230+
return;
2231+
}
2232+
2233+
if (max_response_body_size_ > 0 && buffer->size() > max_response_body_size_) {
2234+
handler(std::unexpected(make_error_code(http_client_error::response_too_large)));
2235+
return;
2236+
}
2237+
2238+
if (ec == asio::error::eof) {
2239+
// Connection closed — body is complete
2240+
std::string body(static_cast<const char*>(buffer->data().data()), buffer->size());
2241+
2242+
response resp;
2243+
resp.status_code = status_code;
2244+
resp.response_headers = std::move(response_headers);
2245+
resp.response_body = std::move(body);
2246+
2247+
handler(std::move(resp));
2248+
return;
2249+
}
2250+
2251+
// More data available — continue reading
2252+
async_read_eof_body(socket_var, buffer, url, use_https, status_code,
2253+
std::move(response_headers), std::move(handler));
2254+
});
2255+
},
2256+
*socket_var);
2257+
}
2258+
22092259
template <typename CompletionHandler>
22102260
void parse_and_read_body(std::shared_ptr<socket_variant> socket_var, std::shared_ptr<asio::streambuf> buffer,
22112261
size_t header_size, const url_parts& url, bool use_https, CompletionHandler&& handler)
@@ -2284,35 +2334,8 @@ namespace glz
22842334
}
22852335
else if (!has_content_length) {
22862336
// No Content-Length and not chunked: read until connection close (RFC 7230 §3.3.3)
2287-
std::visit(
2288-
[&, this](auto& sock) {
2289-
asio::async_read(
2290-
*sock, *buffer, asio::transfer_all(),
2291-
[this, socket_var, buffer, url, use_https, status_code = parsed_status->status_code,
2292-
response_headers = std::move(response_headers),
2293-
handler = std::forward<CompletionHandler>(handler)](asio::error_code ec, std::size_t) mutable {
2294-
if (ec && ec != asio::error::eof) {
2295-
handler(std::unexpected(ec));
2296-
return;
2297-
}
2298-
2299-
if (max_response_body_size_ > 0 && buffer->size() > max_response_body_size_) {
2300-
handler(std::unexpected(make_error_code(http_client_error::response_too_large)));
2301-
return;
2302-
}
2303-
2304-
std::string body(static_cast<const char*>(buffer->data().data()), buffer->size());
2305-
2306-
response resp;
2307-
resp.status_code = status_code;
2308-
resp.response_headers = std::move(response_headers);
2309-
resp.response_body = std::move(body);
2310-
2311-
// Connection is done after EOF-delimited body, don't return to pool
2312-
handler(std::move(resp));
2313-
});
2314-
},
2315-
*socket_var);
2337+
async_read_eof_body(socket_var, buffer, url, use_https, parsed_status->status_code,
2338+
std::move(response_headers), std::forward<CompletionHandler>(handler));
23162339
}
23172340
else {
23182341
// Read the rest of the body based on content-length.

tests/networking_tests/http_client_test/http_client_test.cpp

Lines changed: 102 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,18 +1091,9 @@ class eof_delimited_server
10911091
bool start(const std::string& response_body)
10921092
{
10931093
try {
1094-
for (uint16_t test_port = 18300; test_port < 18400; ++test_port) {
1095-
try {
1096-
acceptor_ = std::make_unique<asio::ip::tcp::acceptor>(
1097-
io_, asio::ip::tcp::endpoint(asio::ip::make_address("127.0.0.1"), test_port));
1098-
port_ = test_port;
1099-
break;
1100-
}
1101-
catch (...) {
1102-
continue;
1103-
}
1104-
}
1105-
if (port_ == 0) return false;
1094+
acceptor_ = std::make_unique<asio::ip::tcp::acceptor>(
1095+
io_, asio::ip::tcp::endpoint(asio::ip::make_address("127.0.0.1"), 0));
1096+
port_ = acceptor_->local_endpoint().port();
11061097

11071098
server_thread_ = std::thread([this, response_body]() {
11081099
try {
@@ -1136,6 +1127,47 @@ class eof_delimited_server
11361127
}
11371128
}
11381129

1130+
bool start_with_reset()
1131+
{
1132+
try {
1133+
acceptor_ = std::make_unique<asio::ip::tcp::acceptor>(
1134+
io_, asio::ip::tcp::endpoint(asio::ip::make_address("127.0.0.1"), 0));
1135+
port_ = acceptor_->local_endpoint().port();
1136+
1137+
server_thread_ = std::thread([this]() {
1138+
try {
1139+
asio::ip::tcp::socket socket(io_);
1140+
acceptor_->accept(socket);
1141+
1142+
asio::streambuf request_buf;
1143+
asio::read_until(socket, request_buf, "\r\n\r\n");
1144+
1145+
// Send headers and partial body, then reset the connection
1146+
std::string http_response =
1147+
"HTTP/1.1 200 OK\r\n"
1148+
"Content-Type: application/json\r\n"
1149+
"Connection: close\r\n"
1150+
"\r\n"
1151+
"partial";
1152+
1153+
asio::write(socket, asio::buffer(http_response));
1154+
1155+
// Force a RST by setting linger to 0 and closing
1156+
asio::socket_base::linger option(true, 0);
1157+
socket.set_option(option);
1158+
socket.close();
1159+
}
1160+
catch (...) {
1161+
}
1162+
});
1163+
1164+
return true;
1165+
}
1166+
catch (...) {
1167+
return false;
1168+
}
1169+
}
1170+
11391171
void stop()
11401172
{
11411173
if (acceptor_) acceptor_->close();
@@ -1197,6 +1229,64 @@ suite eof_delimited_tests = [] {
11971229

11981230
server.stop();
11991231
};
1232+
1233+
"sync_eof_delimited_max_body_size"_test = [] {
1234+
// Body larger than the limit
1235+
const std::string large_body(1024, 'X');
1236+
eof_delimited_server server;
1237+
expect(server.start(large_body)) << "EOF-delimited server should start";
1238+
1239+
glz::http_client client;
1240+
client.max_response_body_size(100); // limit well below body size
1241+
auto result = client.get(server.base_url() + "/api/0/config");
1242+
1243+
expect(!result.has_value()) << "GET should fail when body exceeds max size";
1244+
1245+
server.stop();
1246+
};
1247+
1248+
"async_eof_delimited_max_body_size"_test = [] {
1249+
const std::string large_body(1024, 'X');
1250+
eof_delimited_server server;
1251+
expect(server.start(large_body)) << "EOF-delimited server should start";
1252+
1253+
glz::http_client client;
1254+
client.max_response_body_size(100);
1255+
1256+
std::promise<glz::expected<glz::response, std::error_code>> promise;
1257+
auto future = promise.get_future();
1258+
1259+
client.get_async(server.base_url() + "/api/0/config", {},
1260+
[&](glz::expected<glz::response, std::error_code> result) { promise.set_value(std::move(result)); });
1261+
1262+
auto status = future.wait_for(std::chrono::seconds(5));
1263+
expect(status == std::future_status::ready) << "Async GET should complete";
1264+
1265+
if (status == std::future_status::ready) {
1266+
auto result = future.get();
1267+
expect(!result.has_value()) << "Async GET should fail when body exceeds max size";
1268+
}
1269+
1270+
server.stop();
1271+
};
1272+
1273+
"sync_eof_delimited_connection_reset"_test = [] {
1274+
// Server that resets the connection after sending partial data
1275+
eof_delimited_server server;
1276+
expect(server.start_with_reset()) << "Server should start";
1277+
1278+
glz::http_client client;
1279+
auto result = client.get(server.base_url() + "/api/0/config");
1280+
1281+
// RST behavior is platform-dependent:
1282+
// - On most platforms, the RST is received as a connection error (result is an error)
1283+
// - On some platforms, data may arrive before the RST, resulting in a partial body with EOF
1284+
if (result.has_value()) {
1285+
expect(result->response_body == "partial") << "If RST is seen as EOF, partial body should be returned";
1286+
}
1287+
1288+
server.stop();
1289+
};
12001290
};
12011291

12021292
int main()

0 commit comments

Comments
 (0)