Skip to content

Commit ad61419

Browse files
committed
Refactor streaming API: Replace open_stream_direct() with open_stream() and update related tests
1 parent 0406daf commit ad61419

File tree

4 files changed

+156
-96
lines changed

4 files changed

+156
-96
lines changed

README20.md

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# cpp-httplib C++20 Streaming API
22

3-
This document describes the C++20 streaming extensions for cpp-httplib, providing a generator-like API for handling HTTP responses incrementally.
3+
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

55
## Overview
66

7-
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`. This is particularly useful for:
7+
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:
88

99
- **LLM/AI streaming responses** (e.g., ChatGPT, Claude, Ollama)
1010
- **Server-Sent Events (SSE)**
@@ -42,10 +42,12 @@ int main() {
4242

4343
### Low-Level API: `StreamHandle`
4444

45-
The `StreamHandle` struct provides direct control over streaming responses.
45+
The `StreamHandle` struct provides direct control over streaming responses. It takes ownership of the socket connection and reads data directly from the network.
46+
47+
> **Note:** When using `open_stream()`, the connection is dedicated to streaming and **Keep-Alive is not supported**. For Keep-Alive connections, use `client.Get()` instead.
4648
4749
```cpp
48-
// Open a stream
50+
// Open a stream (takes ownership of socket)
4951
httplib::Client cli("http://localhost:8080");
5052
auto handle = cli.open_stream("/path");
5153

@@ -74,7 +76,8 @@ if (handle.is_valid()) {
7476
| `response` | `std::unique_ptr<Response>` | HTTP response with headers |
7577
| `error` | `Error` | Error code if request failed |
7678
| `is_valid()` | `bool` | Returns true if response is valid |
77-
| `read(buf, len)` | `ssize_t` | Read up to `len` bytes, returns bytes read (0 at EOF, -1 on error) |
79+
| `is_socket_direct_mode()` | `bool` | Returns true (always direct socket reading) |
80+
| `read(buf, len)` | `ssize_t` | Read up to `len` bytes directly from socket |
7881
| `read_all()` | `std::string` | Read all remaining content |
7982
8083
### High-Level API: `GetStream()` and `StreamingResult`
@@ -257,20 +260,39 @@ int main() {
257260
| Feature | `Client::Get()` | `open_stream()` | `GetStream()` |
258261
|---------|----------------|-----------------|---------------|
259262
| Headers available | After complete | Immediately | Immediately |
260-
| Body reading | All at once | Incremental | Generator-based |
261-
| Memory usage | Full body in RAM | Controlled | Controlled |
263+
| Body reading | All at once | Direct from socket | Generator-based |
264+
| Memory usage | Full body in RAM | Minimal (controlled) | Minimal (controlled) |
265+
| Keep-Alive support | ✅ Yes | ❌ No | ❌ No |
266+
| Compression | Auto-handled | Auto-handled | Auto-handled |
262267
| C++ standard | C++11 | C++11 | C++20 |
263-
| Best for | Small responses | Low-level control | Modern streaming |
268+
| Best for | Small responses, Keep-Alive | Low-level streaming | Modern streaming |
269+
270+
## Features
271+
272+
- **True socket-level streaming**: Data is read directly from the network socket
273+
- **Low memory footprint**: Only the current chunk is held in memory
274+
- **Compression support**: Automatic decompression for gzip, brotli, and zstd
275+
- **Chunked transfer**: Full support for chunked transfer encoding
276+
- **SSL/TLS support**: Works with HTTPS connections
277+
- **C++23 ready**: `Generator<T>` is compatible with `std::generator` interface
278+
279+
## Important Notes
264280

265-
## Current Limitations
281+
### Keep-Alive Behavior
266282

267-
> **Note:** The current implementation loads the entire response body into memory before streaming. True socket-level streaming (reading directly from the network) is planned for a future release.
283+
The streaming API (`GetStream()` / `open_stream()`) takes ownership of the socket connection for the duration of the stream. This means:
268284

269-
This means:
285+
- **Keep-Alive is not supported** for streaming connections
286+
- The socket is closed when `StreamHandle` is destroyed
287+
- For Keep-Alive scenarios, use the standard `client.Get()` API instead
270288

271-
- Memory usage is similar to `Client::Get()` for now
272-
- The API is ready for future optimization
273-
- Useful for header-first processing and chunked iteration patterns
289+
```cpp
290+
// Use for streaming (no Keep-Alive)
291+
auto stream = httplib::GetStream(cli, "/large-stream");
292+
293+
// Use for Keep-Alive connections
294+
auto result = cli.Get("/api/data"); // Connection can be reused
295+
```
274296

275297
## Building
276298

httplib.h

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1674,14 +1674,10 @@ class ClientImpl {
16741674
// clang-format on
16751675

16761676
// Streaming API: Open a stream for reading response body incrementally
1677+
// Socket ownership is transferred to StreamHandle for true streaming
16771678
StreamHandle open_stream(const std::string &path);
16781679
StreamHandle open_stream(const std::string &path, const Headers &headers);
16791680

1680-
// True streaming API: Socket ownership transferred to StreamHandle
1681-
StreamHandle open_stream_direct(const std::string &path);
1682-
StreamHandle open_stream_direct(const std::string &path,
1683-
const Headers &headers);
1684-
16851681
bool send(Request &req, Response &res, Error &error);
16861682
Result send(const Request &req);
16871683

@@ -2051,15 +2047,11 @@ class Client {
20512047
// clang-format on
20522048

20532049
// Streaming API: Open a stream for reading response body incrementally
2050+
// Socket ownership is transferred to StreamHandle for true streaming
20542051
ClientImpl::StreamHandle open_stream(const std::string &path);
20552052
ClientImpl::StreamHandle open_stream(const std::string &path,
20562053
const Headers &headers);
20572054

2058-
// True streaming API: Socket ownership transferred to StreamHandle
2059-
ClientImpl::StreamHandle open_stream_direct(const std::string &path);
2060-
ClientImpl::StreamHandle open_stream_direct(const std::string &path,
2061-
const Headers &headers);
2062-
20632055
bool send(Request &req, Response &res, Error &error);
20642056
Result send(const Request &req);
20652057

@@ -9328,31 +9320,6 @@ ClientImpl::open_stream(const std::string &path) {
93289320
inline ClientImpl::StreamHandle
93299321
ClientImpl::open_stream(const std::string &path, const Headers &headers) {
93309322
StreamHandle handle;
9331-
9332-
Request req;
9333-
req.method = "GET";
9334-
req.path = path;
9335-
req.headers = headers;
9336-
9337-
// Use content_receiver to receive body into response
9338-
handle.response = detail::make_unique<Response>();
9339-
handle.error = Error::Success;
9340-
9341-
auto ret = send(req, *handle.response, handle.error);
9342-
if (!ret) { handle.response.reset(); }
9343-
9344-
return handle;
9345-
}
9346-
9347-
inline ClientImpl::StreamHandle
9348-
ClientImpl::open_stream_direct(const std::string &path) {
9349-
return open_stream_direct(path, Headers{});
9350-
}
9351-
9352-
inline ClientImpl::StreamHandle
9353-
ClientImpl::open_stream_direct(const std::string &path,
9354-
const Headers &headers) {
9355-
StreamHandle handle;
93569323
handle.response = detail::make_unique<Response>();
93579324
handle.error = Error::Success;
93589325

@@ -12733,15 +12700,6 @@ inline ClientImpl::StreamHandle Client::open_stream(const std::string &path,
1273312700
return cli_->open_stream(path, headers);
1273412701
}
1273512702

12736-
inline ClientImpl::StreamHandle
12737-
Client::open_stream_direct(const std::string &path) {
12738-
return cli_->open_stream_direct(path);
12739-
}
12740-
inline ClientImpl::StreamHandle
12741-
Client::open_stream_direct(const std::string &path, const Headers &headers) {
12742-
return cli_->open_stream_direct(path, headers);
12743-
}
12744-
1274512703
inline bool Client::send(Request &req, Response &res, Error &error) {
1274612704
return cli_->send(req, res, error);
1274712705
}

httplib20.h

Lines changed: 94 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,67 @@
77
// Copyright (c) 2025 Yuji Hirose. All rights reserved.
88
// MIT License
99
//
10+
// C++23 Migration Notes:
11+
// ----------------------
12+
// When C++23 is adopted, this header can be simplified:
13+
// - Replace custom Generator<T> with std::generator<T> from <generator>
14+
// - The Generator interface is designed to be compatible with std::generator
15+
// - StreamingResult and GetStream() can remain unchanged
16+
//
1017

1118
#ifndef CPPHTTPLIB_HTTPLIB20_H
1219
#define CPPHTTPLIB_HTTPLIB20_H
1320

21+
// Version check: requires C++20 or later
1422
#if __cplusplus < 202002L
1523
#error "httplib20.h requires C++20 or later"
1624
#endif
1725

1826
#include "httplib.h"
27+
1928
#include <coroutine>
2029
#include <cstring>
2130
#include <iterator>
2231
#include <string_view>
2332
#include <utility>
2433

34+
// C++23 feature detection for std::generator
35+
// When available, prefer std::generator over custom implementation
36+
#if defined(__cpp_lib_generator) && __cpp_lib_generator >= 202207L
37+
#include <generator>
38+
#define CPPHTTPLIB_USE_STD_GENERATOR 1
39+
#endif
40+
2541
namespace httplib {
2642

2743
//------------------------------------------------------------------------------
28-
// Generator<T> - A C++20 coroutine-based generator (similar to std::generator)
44+
// Generator<T> - A C++20 coroutine-based generator
2945
//------------------------------------------------------------------------------
46+
//
47+
// This is a simplified implementation compatible with C++23's std::generator.
48+
// Key features:
49+
// - Lazy evaluation: values computed on-demand
50+
// - Range-based for loop support via iterators
51+
// - Move-only semantics (non-copyable)
52+
// - Exception propagation from coroutine body
53+
//
54+
// Usage:
55+
// Generator<int> range(int start, int end) {
56+
// for (int i = start; i < end; ++i) {
57+
// co_yield i;
58+
// }
59+
// }
60+
//
61+
// for (int value : range(0, 10)) {
62+
// std::cout << value << '\n';
63+
// }
64+
//
65+
// C++23 Migration:
66+
// Replace with: using Generator = std::generator;
67+
// Or: #include <generator> and use std::generator directly
68+
//
69+
70+
#ifndef CPPHTTPLIB_USE_STD_GENERATOR
3071

3172
template <typename T> class Generator {
3273
public:
@@ -102,10 +143,11 @@ template <typename T> class Generator {
102143
Handle handle_;
103144
};
104145

146+
// Constructors
105147
Generator() noexcept : handle_(nullptr) {}
106-
107148
explicit Generator(Handle handle) noexcept : handle_(handle) {}
108149

150+
// Move-only semantics
109151
Generator(Generator &&other) noexcept : handle_(other.handle_) {
110152
other.handle_ = nullptr;
111153
}
@@ -126,6 +168,7 @@ template <typename T> class Generator {
126168
if (handle_) { handle_.destroy(); }
127169
}
128170

171+
// Range interface
129172
Iterator begin() {
130173
if (handle_) {
131174
handle_.resume();
@@ -146,13 +189,21 @@ template <typename T> class Generator {
146189
Handle handle_;
147190
};
148191

192+
#else // CPPHTTPLIB_USE_STD_GENERATOR
193+
194+
// C++23: Use std::generator directly
195+
template <typename T> using Generator = std::generator<T>;
196+
197+
#endif // CPPHTTPLIB_USE_STD_GENERATOR
198+
149199
//------------------------------------------------------------------------------
150200
// Streaming client functions using Generator
151201
//------------------------------------------------------------------------------
152202

153203
namespace detail {
154204

155205
// Coroutine that yields chunks from StreamHandle
206+
// Works with both memory buffer mode and socket direct mode
156207
inline Generator<std::string_view> stream_body(ClientImpl::StreamHandle handle,
157208
size_t chunk_size = 8192) {
158209
if (!handle.is_valid()) { co_return; }
@@ -168,8 +219,20 @@ inline Generator<std::string_view> stream_body(ClientImpl::StreamHandle handle,
168219
} // namespace detail
169220

170221
//------------------------------------------------------------------------------
171-
// StreamingResult - Wrapper for streaming response with Generator support
222+
// StreamingResult - High-level wrapper for streaming HTTP responses
172223
//------------------------------------------------------------------------------
224+
//
225+
// Provides a convenient interface for streaming HTTP response bodies.
226+
// Supports both memory-buffered and socket-direct streaming modes.
227+
//
228+
// Usage:
229+
// auto result = httplib::GetStream(client, "/large-file");
230+
// if (result) {
231+
// for (auto chunk : result.body()) {
232+
// process(chunk);
233+
// }
234+
// }
235+
//
173236

174237
class StreamingResult {
175238
public:
@@ -178,17 +241,17 @@ class StreamingResult {
178241
explicit StreamingResult(ClientImpl::StreamHandle &&handle)
179242
: handle_(std::move(handle)) {}
180243

244+
// Move-only semantics
181245
StreamingResult(StreamingResult &&) = default;
182246
StreamingResult &operator=(StreamingResult &&) = default;
183-
184247
StreamingResult(const StreamingResult &) = delete;
185248
StreamingResult &operator=(const StreamingResult &) = delete;
186249

187-
// Check if result is valid
250+
// Validity check
188251
bool is_valid() const { return handle_.is_valid(); }
189252
explicit operator bool() const { return is_valid(); }
190253

191-
// Access response metadata
254+
// Response metadata access
192255
int status() const {
193256
return handle_.response ? handle_.response->status : -1;
194257
}
@@ -210,26 +273,43 @@ class StreamingResult {
210273

211274
Error error() const { return handle_.error; }
212275

213-
// Get body as Generator for streaming reads
276+
// Get read error (socket direct mode only)
277+
Error read_error() const { return handle_.get_read_error(); }
278+
279+
// Streaming body access - returns Generator for lazy iteration
214280
Generator<std::string_view> body(size_t chunk_size = 8192) {
215281
return detail::stream_body(std::move(handle_), chunk_size);
216282
}
217283

218284
// Read entire body at once (convenience method)
219-
std::string read_all() {
220-
if (!handle_.is_valid()) { return {}; }
221-
return handle_.response ? std::move(handle_.response->body) : std::string{};
222-
}
285+
// Note: For large responses, prefer body() for memory efficiency
286+
std::string read_all() { return handle_.read_all(); }
223287

224288
private:
225289
ClientImpl::StreamHandle handle_;
226290
};
227291

228292
//------------------------------------------------------------------------------
229-
// Free functions for streaming requests
293+
// Free functions for streaming HTTP requests
230294
//------------------------------------------------------------------------------
295+
//
296+
// GetStream reads response body directly from the socket without buffering
297+
// the entire response in memory. This is ideal for:
298+
// - Large file downloads
299+
// - Server-Sent Events (SSE)
300+
// - Any streaming API
301+
//
302+
// Note: The connection is not reused (Keep-Alive disabled) since socket
303+
// ownership is transferred to StreamHandle. For repeated small requests
304+
// where connection reuse matters, use client.Get() instead.
305+
//
306+
// Usage:
307+
// auto result = httplib::GetStream(client, "/huge-file");
308+
// for (auto chunk : result.body()) {
309+
// write_to_file(chunk);
310+
// }
311+
//
231312

232-
// GET request with streaming response
233313
inline StreamingResult GetStream(Client &cli, const std::string &path) {
234314
return StreamingResult{cli.open_stream(path)};
235315
}
@@ -239,7 +319,7 @@ inline StreamingResult GetStream(Client &cli, const std::string &path,
239319
return StreamingResult{cli.open_stream(path, headers)};
240320
}
241321

242-
// Overloads for ClientImpl
322+
// Overloads for ClientImpl (direct use without Client wrapper)
243323
inline StreamingResult GetStream(ClientImpl &cli, const std::string &path) {
244324
return StreamingResult{cli.open_stream(path)};
245325
}

0 commit comments

Comments
 (0)