@@ -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.
0 commit comments