@@ -1390,6 +1390,72 @@ class Result {
13901390#endif
13911391};
13921392
1393+ // ClientConnection: Represents ownership of a socket connection
1394+ // Used for true streaming where StreamHandle owns the connection
1395+ struct ClientConnection {
1396+ socket_t sock = INVALID_SOCKET;
1397+ #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
1398+ SSL *ssl = nullptr ;
1399+ #endif
1400+
1401+ bool is_open () const { return sock != INVALID_SOCKET; }
1402+
1403+ // Move-only semantics
1404+ ClientConnection () = default ;
1405+ ~ClientConnection () = default ;
1406+
1407+ ClientConnection (const ClientConnection &) = delete ;
1408+ ClientConnection &operator =(const ClientConnection &) = delete ;
1409+
1410+ ClientConnection (ClientConnection &&other) noexcept
1411+ : sock(other.sock)
1412+ #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
1413+ ,
1414+ ssl (other.ssl)
1415+ #endif
1416+ {
1417+ other.sock = INVALID_SOCKET;
1418+ #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
1419+ other.ssl = nullptr ;
1420+ #endif
1421+ }
1422+
1423+ ClientConnection &operator =(ClientConnection &&other) noexcept {
1424+ if (this != &other) {
1425+ sock = other.sock ;
1426+ #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
1427+ ssl = other.ssl ;
1428+ #endif
1429+ other.sock = INVALID_SOCKET;
1430+ #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
1431+ other.ssl = nullptr ;
1432+ #endif
1433+ }
1434+ return *this ;
1435+ }
1436+ };
1437+
1438+ namespace detail {
1439+
1440+ // BodyReader: Manages incremental reading of HTTP response body
1441+ // Supports both Content-Length and chunked transfer encoding
1442+ struct BodyReader {
1443+ Stream *stream = nullptr ;
1444+ size_t content_length = 0 ;
1445+ size_t bytes_read = 0 ;
1446+ bool chunked = false ;
1447+ bool eof = false ;
1448+
1449+ // For chunked encoding
1450+ size_t current_chunk_remaining = 0 ;
1451+
1452+ // Read up to len bytes into buf
1453+ // Returns bytes read, 0 on EOF, -1 on error
1454+ ssize_t read (char *buf, size_t len);
1455+ };
1456+
1457+ } // namespace detail
1458+
13931459class ClientImpl {
13941460public:
13951461 explicit ClientImpl (const std::string &host);
@@ -1406,41 +1472,67 @@ class ClientImpl {
14061472
14071473 // Streaming handle for reading response body incrementally
14081474 struct StreamHandle {
1475+ // Common fields
14091476 std::unique_ptr<Response> response;
14101477 Error error = Error::Success;
1478+
1479+ // Mode 1: Memory buffer (existing behavior)
14111480 size_t read_offset_ = 0 ;
14121481
1482+ // Mode 2: Socket direct (true streaming)
1483+ std::unique_ptr<ClientConnection> connection_; // Socket ownership
1484+ Stream *stream_ = nullptr ; // Stream for reading
1485+ detail::BodyReader body_reader_; // Body reading state
1486+
14131487 bool is_valid () const {
14141488 return response != nullptr && error == Error::Success;
14151489 }
14161490
1491+ // Check if using socket direct mode (true streaming)
1492+ bool is_socket_direct_mode () const { return stream_ != nullptr ; }
1493+
14171494 // Read up to len bytes into buf, returns number of bytes read (0 at EOF)
1418- // NOTE: Current implementation reads from pre-loaded response body.
1419- // TODO: Implement true streaming by reading directly from socket stream
1420- // to support large responses without loading entire body into memory.
14211495 ssize_t read (char *buf, size_t len) {
14221496 if (!is_valid () || !response) { return -1 ; }
14231497
1424- const auto &body = response->body ;
1425- if (read_offset_ >= body.size ()) { return 0 ; }
1426-
1427- auto remaining = body.size () - read_offset_;
1428- auto to_read = (std::min)(len, remaining);
1429- std::memcpy (buf, body.data () + read_offset_, to_read);
1430- read_offset_ += to_read;
1431- return static_cast <ssize_t >(to_read);
1498+ if (is_socket_direct_mode ()) {
1499+ // Socket direct mode: read from stream via BodyReader
1500+ return body_reader_.read (buf, len);
1501+ } else {
1502+ // Memory buffer mode: read from pre-loaded response body
1503+ const auto &body = response->body ;
1504+ if (read_offset_ >= body.size ()) { return 0 ; }
1505+
1506+ auto remaining = body.size () - read_offset_;
1507+ auto to_read = (std::min)(len, remaining);
1508+ std::memcpy (buf, body.data () + read_offset_, to_read);
1509+ read_offset_ += to_read;
1510+ return static_cast <ssize_t >(to_read);
1511+ }
14321512 }
14331513
14341514 // Read all remaining content into a string
14351515 std::string read_all () {
14361516 if (!is_valid () || !response) { return {}; }
14371517
1438- const auto &body = response->body ;
1439- if (read_offset_ >= body.size ()) { return {}; }
1518+ if (is_socket_direct_mode ()) {
1519+ // Socket direct mode: read all from stream
1520+ std::string result;
1521+ char buf[8192 ];
1522+ ssize_t n;
1523+ while ((n = body_reader_.read (buf, sizeof (buf))) > 0 ) {
1524+ result.append (buf, static_cast <size_t >(n));
1525+ }
1526+ return result;
1527+ } else {
1528+ // Memory buffer mode
1529+ const auto &body = response->body ;
1530+ if (read_offset_ >= body.size ()) { return {}; }
14401531
1441- auto result = body.substr (read_offset_);
1442- read_offset_ = body.size ();
1443- return result;
1532+ auto result = body.substr (read_offset_);
1533+ read_offset_ = body.size ();
1534+ return result;
1535+ }
14441536 }
14451537 };
14461538
@@ -3427,68 +3519,6 @@ inline bool is_socket_alive(socket_t sock) {
34273519 return detail::read_socket (sock, &buf[0 ], sizeof (buf), MSG_PEEK) > 0 ;
34283520}
34293521
3430- // ClientConnection: Represents ownership of a socket connection
3431- // Used for true streaming where StreamHandle owns the connection
3432- struct ClientConnection {
3433- socket_t sock = INVALID_SOCKET;
3434- #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
3435- SSL *ssl = nullptr ;
3436- #endif
3437-
3438- bool is_open () const { return sock != INVALID_SOCKET; }
3439-
3440- // Move-only semantics
3441- ClientConnection () = default ;
3442- ~ClientConnection () = default ;
3443-
3444- ClientConnection (const ClientConnection &) = delete ;
3445- ClientConnection &operator =(const ClientConnection &) = delete ;
3446-
3447- ClientConnection (ClientConnection &&other) noexcept
3448- : sock(other.sock)
3449- #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
3450- ,
3451- ssl (other.ssl)
3452- #endif
3453- {
3454- other.sock = INVALID_SOCKET;
3455- #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
3456- other.ssl = nullptr ;
3457- #endif
3458- }
3459-
3460- ClientConnection &operator =(ClientConnection &&other) noexcept {
3461- if (this != &other) {
3462- sock = other.sock ;
3463- #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
3464- ssl = other.ssl ;
3465- #endif
3466- other.sock = INVALID_SOCKET;
3467- #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
3468- other.ssl = nullptr ;
3469- #endif
3470- }
3471- return *this ;
3472- }
3473- };
3474-
3475- // BodyReader: Manages incremental reading of HTTP response body
3476- // Supports both Content-Length and chunked transfer encoding
3477- struct BodyReader {
3478- Stream *stream = nullptr ;
3479- size_t content_length = 0 ;
3480- size_t bytes_read = 0 ;
3481- bool chunked = false ;
3482- bool eof = false ;
3483-
3484- // For chunked encoding
3485- size_t current_chunk_remaining = 0 ;
3486-
3487- // Read up to len bytes into buf
3488- // Returns bytes read, 0 on EOF, -1 on error
3489- ssize_t read (char *buf, size_t len);
3490- };
3491-
34923522class SocketStream final : public Stream {
34933523public:
34943524 SocketStream (socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec,
0 commit comments