mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-19 11:19:36 +00:00
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.
364 lines
13 KiB
Markdown
364 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.
|
|
|
|
### 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](https://github.com/yhirose/cpp-httplib/issues/2269) - Original feature request
|
|
- [example/ssecli-stream.cc](./example/ssecli-stream.cc) - SSE client with auto-reconnection
|