Skip to content

Commit 1fa9d88

Browse files
committed
Update README and remove read_all() method for improved streaming API clarity
1 parent 4792ead commit 1fa9d88

File tree

4 files changed

+50
-54
lines changed

4 files changed

+50
-54
lines changed

README-stream.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
This document describes the C++20 streaming extensions for cpp-httplib, providing a generator-like API for handling HTTP responses incrementally with **true socket-level streaming**.
44

5+
> **Important Notes**:
6+
>
7+
> - **No Keep-Alive**: Each `stream::Get()` call uses a dedicated connection that is closed after the response is fully read. For connection reuse, use `Client::Get()`.
8+
> - **Single iteration only**: The `body()` generator can only be iterated once. Calling `body()` again after iteration has no effect.
9+
> - **Result is not thread-safe**: While `stream::Get()` can be called from multiple threads simultaneously, the returned `stream::Result` must be used from a single thread only.
10+
511
## Overview
612

713
The C++20 streaming API allows you to process HTTP response bodies chunk by chunk using C++20 coroutines, similar to Python's generators or C++23's `std::generator`. Data is read directly from the network socket, enabling low-memory processing of large responses. This is particularly useful for:
@@ -91,9 +97,6 @@ if (handle.is_valid()) {
9197
while ((n = handle.read(buf, sizeof(buf))) > 0) {
9298
process(buf, n);
9399
}
94-
95-
// Or read all at once
96-
std::string body = handle.read_all();
97100
}
98101
```
99102
@@ -106,7 +109,6 @@ if (handle.is_valid()) {
106109
| `is_valid()` | `bool` | Returns true if response is valid |
107110
| `is_socket_direct_mode()` | `bool` | Returns true (always direct socket reading) |
108111
| `read(buf, len)` | `ssize_t` | Read up to `len` bytes directly from socket |
109-
| `read_all()` | `std::string` | Read all remaining content |
110112
111113
### High-Level API: `stream::Get()` and `stream::Result`
112114
@@ -133,7 +135,6 @@ auto result = httplib::stream::Get(cli, "/path", headers);
133135
| `status()` | `int` | HTTP status code |
134136
| `headers()` | `Headers&` | Response headers |
135137
| `body(chunk_size)` | `Generator<std::string_view>` | Generator yielding body chunks |
136-
| `read_all()` | `std::string` | Read entire body at once |
137138

138139
### Generator Class
139140

httplib-stream.h

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,10 +284,6 @@ class Result {
284284
return detail::stream_body(std::move(handle_), chunk_size);
285285
}
286286

287-
// Read entire body at once (convenience method)
288-
// Note: For large responses, prefer body() for memory efficiency
289-
std::string read_all() { return handle_.read_all(); }
290-
291287
private:
292288
ClientImpl::StreamHandle handle_;
293289
};

httplib.h

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1545,31 +1545,6 @@ class ClientImpl {
15451545
ssize_t read_with_decompression(char *buf, size_t len);
15461546

15471547
public:
1548-
// Read all remaining content into a string
1549-
std::string read_all() {
1550-
if (!is_valid() || !response) { return {}; }
1551-
1552-
if (is_socket_direct_mode()) {
1553-
// Socket direct mode: read all from stream (uses read() for
1554-
// decompression)
1555-
std::string result;
1556-
char buf[8192];
1557-
ssize_t n;
1558-
while ((n = read(buf, sizeof(buf))) > 0) {
1559-
result.append(buf, static_cast<size_t>(n));
1560-
}
1561-
return result;
1562-
} else {
1563-
// Memory buffer mode
1564-
const auto &body = response->body;
1565-
if (read_offset_ >= body.size()) { return {}; }
1566-
1567-
auto result = body.substr(read_offset_);
1568-
read_offset_ = body.size();
1569-
return result;
1570-
}
1571-
}
1572-
15731548
// Get the last error that occurred during reading (socket direct mode only)
15741549
Error get_read_error() const { return body_reader_.last_error; }
15751550

test/test-stream.cc

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,21 @@
66
#define CPPHTTPLIB_OPENSSL_SUPPORT
77
#include "../httplib.h"
88

9+
//------------------------------------------------------------------------------
10+
// Test helper functions
11+
//------------------------------------------------------------------------------
12+
13+
// Helper function to read all content from a StreamHandle
14+
inline std::string read_all(httplib::ClientImpl::StreamHandle &handle) {
15+
std::string result;
16+
char buf[8192];
17+
ssize_t n;
18+
while ((n = handle.read(buf, sizeof(buf))) > 0) {
19+
result.append(buf, static_cast<size_t>(n));
20+
}
21+
return result;
22+
}
23+
924
//------------------------------------------------------------------------------
1025
// Step 1: StreamHandle struct existence test
1126
//------------------------------------------------------------------------------
@@ -262,13 +277,16 @@ TEST_F(StreamingServerTest, stream_Get_BodyGeneratorSmallChunks) {
262277
EXPECT_GT(chunk_count, 1u); // Should have multiple chunks
263278
}
264279

265-
TEST_F(StreamingServerTest, stream_Get_ReadAll) {
280+
TEST_F(StreamingServerTest, stream_Get_BodyIteration) {
266281
httplib::Client cli("localhost", 8787);
267282

268283
auto result = httplib::stream::Get(cli, "/hello");
269284
ASSERT_TRUE(result.is_valid());
270285

271-
std::string body = result.read_all();
286+
std::string body;
287+
for (auto chunk : result.body()) {
288+
body.append(chunk);
289+
}
272290
EXPECT_EQ("Hello World!", body);
273291
}
274292

@@ -299,7 +317,10 @@ TEST_F(StreamingServerTest, stream_Get_WithParams) {
299317
ASSERT_TRUE(result.is_valid());
300318
EXPECT_EQ(200, result.status());
301319

302-
std::string body = result.read_all();
320+
std::string body;
321+
for (auto chunk : result.body()) {
322+
body.append(chunk);
323+
}
303324
EXPECT_TRUE(body.find("foo=bar") != std::string::npos);
304325
EXPECT_TRUE(body.find("baz=123") != std::string::npos);
305326
}
@@ -314,7 +335,10 @@ TEST_F(StreamingServerTest, stream_Get_WithParamsAndHeaders) {
314335
ASSERT_TRUE(result.is_valid());
315336
EXPECT_EQ(200, result.status());
316337

317-
std::string body = result.read_all();
338+
std::string body;
339+
for (auto chunk : result.body()) {
340+
body.append(chunk);
341+
}
318342
EXPECT_TRUE(body.find("key=value") != std::string::npos);
319343
}
320344

@@ -431,7 +455,7 @@ TEST_F(ChunkedStreamingTest, ReadChunkedResponse) {
431455
EXPECT_EQ("text/plain", handle.response->get_header_value("Content-Type"));
432456

433457
// Read all content
434-
std::string body = handle.read_all();
458+
std::string body = read_all(handle);
435459
EXPECT_EQ("chunk1\nchunk2\nchunk3\n", body);
436460
}
437461

@@ -488,7 +512,7 @@ TEST_F(ChunkedStreamingTest, SSELikeStreaming) {
488512
EXPECT_EQ("text/event-stream",
489513
handle.response->get_header_value("Content-Type"));
490514

491-
std::string body = handle.read_all();
515+
std::string body = read_all(handle);
492516

493517
// Verify SSE format
494518
EXPECT_NE(std::string::npos, body.find("data: message 0"));
@@ -937,7 +961,7 @@ TEST_F(StreamHandleV2Test, ReadAllSocketDirect) {
937961
handle.body_reader_.stream = mock_stream_.get();
938962
handle.body_reader_.content_length = 18;
939963

940-
auto result = handle.read_all();
964+
auto result = read_all(handle);
941965
EXPECT_EQ("Hello from socket!", result);
942966
}
943967

@@ -956,7 +980,7 @@ TEST_F(StreamHandleV2Test, GetReadErrorReturnsSuccess) {
956980
EXPECT_EQ(httplib::Error::Success, handle.get_read_error());
957981

958982
// Read successfully
959-
handle.read_all();
983+
read_all(handle);
960984
EXPECT_FALSE(handle.has_read_error());
961985
}
962986

@@ -1094,7 +1118,7 @@ TEST_F(OpenStreamDirectTest, ReadBody) {
10941118
auto handle = cli.open_stream("/hello");
10951119
ASSERT_TRUE(handle.is_valid());
10961120

1097-
auto body = handle.read_all();
1121+
auto body = read_all(handle);
10981122
EXPECT_EQ("Hello World!", body);
10991123
}
11001124

@@ -1120,7 +1144,7 @@ TEST_F(OpenStreamDirectTest, LargeResponse) {
11201144
auto handle = cli.open_stream("/large");
11211145
ASSERT_TRUE(handle.is_valid());
11221146

1123-
auto body = handle.read_all();
1147+
auto body = read_all(handle);
11241148
EXPECT_EQ(10000u, body.size());
11251149
EXPECT_EQ(std::string(10000, 'X'), body);
11261150
}
@@ -1141,7 +1165,7 @@ TEST_F(OpenStreamDirectTest, ChunkedResponse) {
11411165
ASSERT_TRUE(handle.is_valid());
11421166
EXPECT_TRUE(handle.body_reader_.chunked);
11431167

1144-
auto body = handle.read_all();
1168+
auto body = read_all(handle);
11451169
// Server sends "chunk" 3 times
11461170
EXPECT_EQ("chunkchunkchunk", body);
11471171
}
@@ -1185,7 +1209,7 @@ TEST_F(OpenStreamDirectTest, GzipCompressedResponse) {
11851209
// Decompressor should be set up
11861210
EXPECT_TRUE(handle.decompressor_ != nullptr);
11871211

1188-
auto body = handle.read_all();
1212+
auto body = read_all(handle);
11891213
EXPECT_EQ("This is gzip compressed chunked data!", body);
11901214
}
11911215

@@ -1222,7 +1246,7 @@ TEST_F(OpenStreamDirectTest, NoCompressionWhenNotRequested) {
12221246
// Should not have decompressor
12231247
EXPECT_TRUE(handle.decompressor_ == nullptr);
12241248

1225-
auto body = handle.read_all();
1249+
auto body = read_all(handle);
12261250
EXPECT_EQ("This is gzip compressed chunked data!", body);
12271251
}
12281252
#endif // CPPHTTPLIB_ZLIB_SUPPORT
@@ -1246,7 +1270,7 @@ TEST_F(OpenStreamDirectTest, BrotliCompressedResponse) {
12461270
// Decompressor should be set up
12471271
EXPECT_TRUE(handle.decompressor_ != nullptr);
12481272

1249-
auto body = handle.read_all();
1273+
auto body = read_all(handle);
12501274
EXPECT_EQ("This is gzip compressed chunked data!", body);
12511275
}
12521276

@@ -1289,7 +1313,7 @@ TEST_F(OpenStreamDirectTest, ZstdCompressedResponse) {
12891313
// Decompressor should be set up
12901314
EXPECT_TRUE(handle.decompressor_ != nullptr);
12911315

1292-
auto body = handle.read_all();
1316+
auto body = read_all(handle);
12931317
EXPECT_EQ("This is gzip compressed chunked data!", body);
12941318
}
12951319

@@ -1332,7 +1356,7 @@ TEST_F(OpenStreamDirectTest, LargeGzipCompressedResponse) {
13321356
EXPECT_TRUE(handle.decompressor_ != nullptr);
13331357

13341358
// Read and decompress 100KB of data
1335-
auto body = handle.read_all();
1359+
auto body = read_all(handle);
13361360
EXPECT_EQ(100 * 1024, body.size());
13371361

13381362
// Verify content pattern
@@ -1377,7 +1401,7 @@ TEST_F(OpenStreamDirectTest, LargeBrotliCompressedResponse) {
13771401
EXPECT_EQ("br", encoding);
13781402
EXPECT_TRUE(handle.decompressor_ != nullptr);
13791403

1380-
auto body = handle.read_all();
1404+
auto body = read_all(handle);
13811405
EXPECT_EQ(100 * 1024, body.size());
13821406
EXPECT_TRUE(body.find("Line 000000: Hello World!") != std::string::npos);
13831407
}
@@ -1398,7 +1422,7 @@ TEST_F(OpenStreamDirectTest, LargeZstdCompressedResponse) {
13981422
EXPECT_EQ("zstd", encoding);
13991423
EXPECT_TRUE(handle.decompressor_ != nullptr);
14001424

1401-
auto body = handle.read_all();
1425+
auto body = read_all(handle);
14021426
EXPECT_EQ(100 * 1024, body.size());
14031427
EXPECT_TRUE(body.find("Line 000000: Hello World!") != std::string::npos);
14041428
}
@@ -1453,7 +1477,7 @@ TEST_F(SSLOpenStreamDirectTest, BasicSSLStream) {
14531477
EXPECT_EQ(200, handle.response->status);
14541478
EXPECT_TRUE(handle.is_socket_direct_mode());
14551479

1456-
auto body = handle.read_all();
1480+
auto body = read_all(handle);
14571481
EXPECT_EQ("Hello SSL World!", body);
14581482
}
14591483

@@ -1466,7 +1490,7 @@ TEST_F(SSLOpenStreamDirectTest, SSLChunkedResponse) {
14661490
ASSERT_TRUE(handle.is_valid()) << "Error: " << static_cast<int>(handle.error);
14671491
EXPECT_TRUE(handle.body_reader_.chunked);
14681492

1469-
auto body = handle.read_all();
1493+
auto body = read_all(handle);
14701494
EXPECT_EQ("chunkchunkchunk", body);
14711495
}
14721496
#endif

0 commit comments

Comments
 (0)