Files
cpp-httplib/README-stream.md
T
yhirose 9bfd2ddf1c Fix README and docs pages that no longer match the code
Bring the READMEs and the docs pages (en and ja) in line with the
current header:

- Fix the client sample that does not compile, the Transfer-Encoding
  statement for default headers, the verifier callback comment, the
  thread pool spawn rule, the sanitize_filename description, and the
  inverted Accept-Encoding sentence.
- Describe recent behaviour: TLS errors behind Read/Write failures,
  Host validation, Connection: close for a body without length,
  content_reader rules, refused CONNECT, WebSocketClient proxies,
  CPPHTTPLIB_INTERIM_RESPONSE_MAX_COUNT, the SSE event size limit, and
  the payload limit of the Stream API.
- Complete the API references of WebSocket, WebSocketClient and the
  stream:: helpers, and the SSE reconnect rules.
- Fix cookbook pages: keychain loading on every backend, received Close
  status, a multipart sample that trusted the client's file name, an
  echo sample that sent binary as text, SNI, pre-request handler scope,
  stop() with WebSocket handlers, and the thread pool notes.
- Restore the mutex in the LLM app's Chapter 5 code.
2026-10-18 10:13:57 -04:00

360 lines
13 KiB
Markdown

# cpp-httplib Streaming API
This document describes the streaming extensions for cpp-httplib, providing an iterator-style API for handling HTTP responses incrementally with **true socket-level streaming**.
> **Important Notes**:
>
> - **No Keep-Alive**: Each `stream::Get()` call uses a dedicated connection that is closed when the returned `stream::Result` is destroyed. For connection reuse, use `Client::Get()`.
> - **Single iteration only**: The `next()` method can only iterate through the body once.
> - **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.
## Overview
The streaming API allows you to process HTTP response bodies chunk by chunk using an iterator-style pattern. Data is read directly from the network socket, enabling low-memory processing of large responses. This is particularly useful for:
- **LLM/AI streaming responses** (e.g., ChatGPT, Claude, Ollama)
- **Server-Sent Events (SSE)**
- **Large file downloads** with progress tracking (mind the [payload size limit](#payload-size-limit))
- **Reverse proxy implementations**
## Quick Start
```cpp
#include "httplib.h"
int main() {
httplib::Client cli("http://localhost:8080");
// Get streaming response
auto result = httplib::stream::Get(cli, "/stream");
if (result) {
// Process response body in chunks
while (result.next()) {
std::cout.write(result.data(), result.size());
}
}
return 0;
}
```
## API Layers
cpp-httplib provides multiple API layers for different use cases:
```text
┌─────────────────────────────────────────────┐
│ SSEClient │ ← SSE-specific, parsed events
│ - on_message(), on_event() │
│ - Auto-reconnect, Last-Event-ID │
├─────────────────────────────────────────────┤
│ stream::Get() / stream::Result │ ← Iterator-based streaming
│ - while (result.next()) { ... } │
├─────────────────────────────────────────────┤
│ open_stream() / StreamHandle │ ← General-purpose streaming
│ - handle.read(buf, len) │
├─────────────────────────────────────────────┤
│ Client::Get() │ ← Traditional, full buffering
└─────────────────────────────────────────────┘
```
| Use Case | Recommended API |
|----------|----------------|
| SSE with auto-reconnect | `SSEClient` (see [README-sse.md](README-sse.md)) |
| LLM streaming (JSON Lines) | `stream::Post()` or `stream::Get()` |
| Large file download | `stream::Get()` or `open_stream()` |
| Reverse proxy | `open_stream()` |
| Small responses with Keep-Alive | `Client::Get()` |
## API Reference
### Low-Level API: `StreamHandle`
The `StreamHandle` struct provides direct control over streaming responses. It takes ownership of the socket connection and reads data directly from the network.
> **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.
```cpp
// Open a stream (takes ownership of socket)
httplib::Client cli("http://localhost:8080");
auto handle = cli.open_stream("GET", "/path");
// Check validity
if (handle.is_valid()) {
// Access response headers immediately
int status = handle.response->status;
auto content_type = handle.response->get_header_value("Content-Type");
// Read body incrementally
char buf[4096];
ssize_t n;
while ((n = handle.read(buf, sizeof(buf))) > 0) {
process(buf, n);
}
}
```
#### StreamHandle Members
| Member | Type | Description |
|--------|------|-------------|
| `response` | `std::unique_ptr<Response>` | HTTP response with headers |
| `error` | `Error` | Error code if request failed |
| `is_valid()` | `bool` | Returns true if response is valid |
| `read(buf, len)` | `ssize_t` | Read up to `len` bytes directly from socket |
| `get_read_error()` | `Error` | Get the last read error |
| `has_read_error()` | `bool` | Check if a read error occurred |
For a chunked response, the trailers are in `response->trailers` once `read()` has reached the end of the body.
### High-Level API: `stream::Get()` and `stream::Result`
The `httplib.h` header provides a more ergonomic iterator-style API.
```cpp
#include "httplib.h"
httplib::Client cli("http://localhost:8080");
...
// Simple GET
auto result = httplib::stream::Get(cli, "/path");
// GET with custom headers
httplib::Headers headers = {{"Authorization", "Bearer token"}};
auto result = httplib::stream::Get(cli, "/path", headers);
// Process the response
if (result) {
while (result.next()) {
process(result.data(), result.size());
}
}
// Or read entire body at once
auto result2 = httplib::stream::Get(cli, "/path");
if (result2) {
std::string body = result2.read_all();
}
```
#### Helpers
There is a helper for each HTTP method. The arguments in brackets can be left out, and every helper takes a trailing `size_t chunk_size = 8192`, the most bytes one `next()` returns.
| Helper | Arguments |
|--------|-----------|
| `stream::Get()` | `(cli, path, [params], [headers], [chunk_size])` |
| `stream::Head()` | `(cli, path, [params], [headers], [chunk_size])` |
| `stream::Options()` | `(cli, path, [params], [headers], [chunk_size])` |
| `stream::Post()` | `(cli, path, [params], [headers], body, content_type, [chunk_size])` |
| `stream::Put()` | `(cli, path, [params], [headers], body, content_type, [chunk_size])` |
| `stream::Patch()` | `(cli, path, [params], [headers], body, content_type, [chunk_size])` |
| `stream::Delete()` | `(cli, path, [params], [headers], [chunk_size])` |
| `stream::Delete()` | `(cli, path, [params], [headers], body, content_type, [chunk_size])` |
`params` is a `Params`, `headers` a `Headers`, and `body` and `content_type` are `std::string`s.
```cpp
// POST a JSON body and stream the response in 4KB chunks
auto result3 = httplib::stream::Post(cli, "/api/generate", headers,
R"({"prompt": "hello"})",
"application/json", 4096);
```
#### stream::Result Members
| Member | Type | Description |
|--------|------|-------------|
| `operator bool()` | `bool` | Returns true if response is valid |
| `is_valid()` | `bool` | Same as `operator bool()` |
| `status()` | `int` | HTTP status code |
| `headers()` | `const Headers&` | Response headers |
| `get_header_value(key, def)` | `std::string` | Get header value (with optional default) |
| `has_header(key)` | `bool` | Check if header exists |
| `next()` | `bool` | Read next chunk, returns false when done |
| `data()` | `const char*` | Pointer to current chunk data |
| `size()` | `size_t` | Size of current chunk |
| `read_all()` | `std::string` | Read entire remaining body into string |
| `error()` | `Error` | Get the connection/request error |
| `read_error()` | `Error` | Get the last read error |
| `has_read_error()` | `bool` | Check if a read error occurred |
## Usage Examples
### Example 1: SSE (Server-Sent Events) Client
```cpp
#include "httplib.h"
#include <iostream>
int main() {
httplib::Client cli("http://localhost:1234");
auto result = httplib::stream::Get(cli, "/events");
if (!result) { return 1; }
while (result.next()) {
std::cout.write(result.data(), result.size());
std::cout.flush();
}
return 0;
}
```
For a complete SSE client with auto-reconnection and event parsing, see `example/ssecli-stream.cc`.
### Example 2: LLM Streaming Response
```cpp
#include "httplib.h"
#include <iostream>
int main() {
httplib::Client cli("http://localhost:11434"); // Ollama
auto result = httplib::stream::Get(cli, "/api/generate");
if (result && result.status() == 200) {
while (result.next()) {
std::cout.write(result.data(), result.size());
std::cout.flush();
}
}
// Check for connection errors
if (result.read_error() != httplib::Error::Success) {
std::cerr << "Connection lost\n";
}
return 0;
}
```
### Example 3: Large File Download with Progress
```cpp
#include "httplib.h"
#include <fstream>
#include <iostream>
int main() {
httplib::Client cli("http://example.com");
cli.set_payload_max_length(0); // No limit (the default is 100MB)
auto result = httplib::stream::Get(cli, "/large-file.zip");
if (!result || result.status() != 200) {
std::cerr << "Download failed\n";
return 1;
}
std::ofstream file("download.zip", std::ios::binary);
size_t total = 0;
while (result.next()) {
file.write(result.data(), result.size());
total += result.size();
std::cout << "\rDownloaded: " << (total / 1024) << " KB" << std::flush;
}
if (result.has_read_error()) {
std::cerr << "\nDownload failed: "
<< httplib::to_string(result.read_error()) << "\n";
return 1;
}
std::cout << "\nComplete!\n";
return 0;
}
```
### Example 4: Reverse Proxy Streaming
```cpp
#include "httplib.h"
// Keeps the client and its stream alive until the response has been sent.
struct Upstream {
httplib::Client cli{"http://backend:8080"};
httplib::ClientImpl::StreamHandle handle;
};
httplib::Server svr;
svr.Get("/proxy/(.*)", [](const httplib::Request& req, httplib::Response& res) {
auto upstream = std::make_shared<Upstream>();
upstream->handle = upstream->cli.open_stream("GET", "/" + req.matches[1].str());
if (!upstream->handle.is_valid()) {
res.status = 502;
return;
}
res.status = upstream->handle.response->status;
res.set_chunked_content_provider(
upstream->handle.response->get_header_value("Content-Type"),
[upstream](size_t, httplib::DataSink& sink) {
char buf[8192];
auto n = upstream->handle.read(buf, sizeof(buf));
if (n > 0) {
sink.write(buf, static_cast<size_t>(n));
return true;
}
sink.done();
return true;
}
);
});
svr.listen("0.0.0.0", 3000);
```
## Comparison with Existing APIs
| Feature | `Client::Get()` | `open_stream()` | `stream::Get()` |
|---------|----------------|-----------------|----------------|
| Headers available | After complete | Immediately | Immediately |
| Body reading | All at once | Direct from socket | Iterator-based |
| Memory usage | Full body in RAM | Minimal (controlled) | Minimal (controlled) |
| Keep-Alive support | ✅ Yes | ❌ No | ❌ No |
| Compression | Auto-handled | Auto-handled | Auto-handled |
| Best for | Small responses, Keep-Alive | Low-level streaming | Easy streaming |
## Features
- **True socket-level streaming**: Data is read directly from the network socket
- **Low memory footprint**: Only the current chunk is held in memory
- **Compression support**: Automatic decompression for gzip, brotli, and zstd
- **Chunked transfer**: Full support for chunked transfer encoding
- **SSL/TLS support**: Works with HTTPS connections
## Important Notes
### Keep-Alive Behavior
The streaming API (`stream::Get()` / `open_stream()`) takes ownership of the socket connection for the duration of the stream. This means:
- **Keep-Alive is not supported** for streaming connections
- The socket is closed when `StreamHandle` is destroyed
- For Keep-Alive scenarios, use the standard `client.Get()` API instead
```cpp
// Use for streaming (no Keep-Alive)
auto result = httplib::stream::Get(cli, "/large-stream");
while (result.next()) { /* ... */ }
// Use for Keep-Alive connections
auto res = cli.Get("/api/data"); // Connection can be reused
```
### Payload Size Limit
`open_stream()` and the `stream::` helpers apply the client's `set_payload_max_length()` (100MB by default, `CPPHTTPLIB_PAYLOAD_MAX_LENGTH`) to the body, both as received and after decompression. Past the limit, `read()` and `next()` stop, and the read error is `Error::ExceedMaxPayloadSize`. To download something larger, raise the limit, or call `cli.set_payload_max_length(0)` for no limit.
## Related
- [Issue #2269](https://github.com/yhirose/cpp-httplib/issues/2269) - Original feature request
- [example/ssecli-stream.cc](./example/ssecli-stream.cc) - SSE client with auto-reconnection