open_stream(), and with it stream::Get() and SSEClient, wrote its own request head and left out what a buffered request gets: the Authorization from set_basic_auth() or set_bearer_token_auth(), and through a plain HTTP proxy the absolute-form target (RFC 9112 3.2.2) and Proxy-Authorization. A proxy refused such a request, and an origin that needs credentials answered 401. Move the two decisions out of write_request() and handle_request() into add_credential_headers() and proxy_target_prefix(), and use them in open_stream() too, after connecting as a buffered request does. A Digest challenge or a redirect is still handed to the caller.
13 KiB
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 returnedstream::Resultis destroyed. For connection reuse, useClient::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 returnedstream::Resultmust 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)
- Reverse proxy implementations
Quick Start
#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:
┌─────────────────────────────────────────────┐
│ 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) |
| 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, useclient.Get()instead.
// 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.
#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::strings.
// 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
#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
#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
#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
#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
StreamHandleis destroyed - For Keep-Alive scenarios, use the standard
client.Get()API instead
// 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.
Authentication and Proxies
A stream request carries the same credentials as client.Get(): set_basic_auth() and set_bearer_token_auth() for the origin, and the proxy's own through set_proxy(), over HTTP or through a CONNECT tunnel. It does not answer a Digest challenge or follow a redirect: the 401 or 3xx response is handed to you.
Related
- Issue #2269 - Original feature request
- example/ssecli-stream.cc - SSE client with auto-reconnection