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.
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:
#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; }
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() |
StreamHandleThe 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); } }
| 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.
stream::Get() and stream::ResultThe 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(); }
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);
| 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 |
#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.
#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; }
#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; }
#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);
| 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 |
The streaming API (stream::Get() / open_stream()) takes ownership of the socket connection for the duration of the stream. This means:
StreamHandle is destroyedclient.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
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.