Files
cpp-httplib/README-stream.md
yhirose 05f7478523 Fix README samples and statements that no longer match the code
Samples that did not compile or did not do what they showed:
- SSLServer has no default constructor, and SSLClient("host:port") does
  not parse the port.
- res.user_data.get<T>() inside a generic lambda needs the `template`
  keyword; use explicit parameter types instead.
- get_header_value() takes the default value before the index.
- Server has no socket(); show set_socket_opt inside set_socket_options.
- The content provider sample used a local without capturing it.
- "*.dev.local" is not a NO_PROXY pattern; ".dev.local" is.
- The reverse proxy example in README-stream.md called open_stream()
  without a method and moved a StreamHandle into a std::function. Keep
  the client and the handle in one shared object.
- open_stream() does not follow redirects, so drop set_follow_location()
  from the Stream API samples.

Statements corrected:
- ssl_error() returns a tls::ErrorCode, not SSL_ERROR_*.
- An exception escaping the exception handler no longer crashes the
  server; the connection is dropped and the error logger is told.
- post_routing_handler runs before every response, including those the
  earlier hooks short-circuit; file_request_handler runs for GET only.
- Only wolfSSL cannot enumerate CAs loaded from a path or the system.
- Digest authentication and the WebSocket TLS setters need any TLS
  backend, not OpenSSL specifically.
- A dynamic pool thread exits on the idle timeout only.
- The stream connection closes when the Result is destroyed.
- The pong timeout takes two to three ping intervals to fire.
- Regex routes skip paths over CPPHTTPLIB_REGEX_ROUTE_PATH_MAX_LENGTH.

Also list the Error values and compressible MIME types that were
missing, note that a content receiver lifts the default client payload
limit, and update the docker server's startup output.
2026-10-08 20:39:13 -04:00

11 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 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
  • 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::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.

// 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

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();
}

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");
    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;
    }
    
    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 StreamHandle is 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