Skip to content

Commit cee57e5

Browse files
committed
Add ClientConnection and BodyReader structs with tests for socket streaming
1 parent ae407bd commit cee57e5

File tree

2 files changed

+212
-84
lines changed

2 files changed

+212
-84
lines changed

httplib.h

Lines changed: 108 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
13931459
class ClientImpl {
13941460
public:
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-
34923522
class SocketStream final : public Stream {
34933523
public:
34943524
SocketStream(socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec,

test/test20.cc

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -518,35 +518,35 @@ TEST_F(ChunkedStreamingTest, SSELikeWithGenerator) {
518518

519519
TEST(ClientConnectionTest, StructExists) {
520520
// Verify ClientConnection struct is defined
521-
httplib::detail::ClientConnection conn;
521+
httplib::ClientConnection conn;
522522

523523
// Check default state
524524
EXPECT_EQ(INVALID_SOCKET, conn.sock);
525525
EXPECT_FALSE(conn.is_open());
526526
}
527527

528528
TEST(ClientConnectionTest, IsOpenReturnsTrueWhenSocketValid) {
529-
httplib::detail::ClientConnection conn;
529+
httplib::ClientConnection conn;
530530
conn.sock = 1; // Fake valid socket
531531

532532
EXPECT_TRUE(conn.is_open());
533533
}
534534

535535
TEST(ClientConnectionTest, MoveConstructor) {
536-
httplib::detail::ClientConnection conn1;
536+
httplib::ClientConnection conn1;
537537
conn1.sock = 42;
538538

539-
httplib::detail::ClientConnection conn2(std::move(conn1));
539+
httplib::ClientConnection conn2(std::move(conn1));
540540

541541
EXPECT_EQ(42, conn2.sock);
542542
EXPECT_EQ(INVALID_SOCKET, conn1.sock); // Moved-from state
543543
}
544544

545545
TEST(ClientConnectionTest, MoveAssignment) {
546-
httplib::detail::ClientConnection conn1;
546+
httplib::ClientConnection conn1;
547547
conn1.sock = 42;
548548

549-
httplib::detail::ClientConnection conn2;
549+
httplib::ClientConnection conn2;
550550
conn2 = std::move(conn1);
551551

552552
EXPECT_EQ(42, conn2.sock);
@@ -693,3 +693,101 @@ TEST(BodyReaderTest, ReadWithoutStream) {
693693

694694
EXPECT_EQ(-1, n);
695695
}
696+
697+
// =============================================================================
698+
// StreamHandle v2 Tests (Socket Direct Mode)
699+
// =============================================================================
700+
701+
class StreamHandleV2Test : public ::testing::Test {
702+
protected:
703+
void SetUp() override {
704+
mock_stream_ = std::make_unique<MockStream>("Hello from socket!");
705+
}
706+
707+
std::unique_ptr<MockStream> mock_stream_;
708+
};
709+
710+
TEST_F(StreamHandleV2Test, SocketDirectModeBasic) {
711+
// Create StreamHandle with socket direct mode
712+
httplib::ClientImpl::StreamHandle handle;
713+
714+
// Set up response (headers only, body will be read from stream)
715+
handle.response = std::make_unique<httplib::Response>();
716+
handle.response->status = 200;
717+
handle.response->set_header("Content-Length", "18");
718+
719+
// Set up socket direct mode
720+
handle.stream_ = mock_stream_.get();
721+
handle.body_reader_.stream = mock_stream_.get();
722+
handle.body_reader_.content_length = 18;
723+
724+
EXPECT_TRUE(handle.is_valid());
725+
EXPECT_TRUE(handle.is_socket_direct_mode());
726+
727+
// Read from socket
728+
char buf[32];
729+
auto n = handle.read(buf, sizeof(buf));
730+
EXPECT_EQ(18, n);
731+
EXPECT_EQ(std::string("Hello from socket!"),
732+
std::string(buf, static_cast<size_t>(n)));
733+
734+
// EOF
735+
n = handle.read(buf, sizeof(buf));
736+
EXPECT_EQ(0, n);
737+
}
738+
739+
TEST_F(StreamHandleV2Test, SocketDirectModeChunkedRead) {
740+
httplib::ClientImpl::StreamHandle handle;
741+
742+
handle.response = std::make_unique<httplib::Response>();
743+
handle.response->status = 200;
744+
745+
handle.stream_ = mock_stream_.get();
746+
handle.body_reader_.stream = mock_stream_.get();
747+
handle.body_reader_.content_length = 18;
748+
749+
// Read in small chunks
750+
char buf[5];
751+
std::string result;
752+
753+
ssize_t n;
754+
while ((n = handle.read(buf, sizeof(buf))) > 0) {
755+
result.append(buf, static_cast<size_t>(n));
756+
}
757+
758+
EXPECT_EQ("Hello from socket!", result);
759+
EXPECT_EQ(0, n); // EOF
760+
}
761+
762+
TEST_F(StreamHandleV2Test, MemoryBufferModeStillWorks) {
763+
// Existing behavior: read from response->body
764+
httplib::ClientImpl::StreamHandle handle;
765+
766+
handle.response = std::make_unique<httplib::Response>();
767+
handle.response->status = 200;
768+
handle.response->body = "Memory buffer content";
769+
770+
// No stream set = memory buffer mode
771+
EXPECT_TRUE(handle.is_valid());
772+
EXPECT_FALSE(handle.is_socket_direct_mode());
773+
774+
char buf[32];
775+
auto n = handle.read(buf, sizeof(buf));
776+
EXPECT_EQ(21, n);
777+
EXPECT_EQ(std::string("Memory buffer content"),
778+
std::string(buf, static_cast<size_t>(n)));
779+
}
780+
781+
TEST_F(StreamHandleV2Test, ReadAllSocketDirect) {
782+
httplib::ClientImpl::StreamHandle handle;
783+
784+
handle.response = std::make_unique<httplib::Response>();
785+
handle.response->status = 200;
786+
787+
handle.stream_ = mock_stream_.get();
788+
handle.body_reader_.stream = mock_stream_.get();
789+
handle.body_reader_.content_length = 18;
790+
791+
auto result = handle.read_all();
792+
EXPECT_EQ("Hello from socket!", result);
793+
}

0 commit comments

Comments
 (0)