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.
This commit is contained in:
yhirose
2026-10-08 00:40:16 -04:00
parent f6aee694e2
commit 05f7478523
3 changed files with 66 additions and 56 deletions

View File

@@ -4,7 +4,7 @@ This document describes the streaming extensions for cpp-httplib, providing an i
> **Important Notes**:
>
> - **No Keep-Alive**: Each `stream::Get()` call uses a dedicated connection that is closed after the response is fully read. For connection reuse, use `Client::Get()`.
> - **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.
@@ -114,7 +114,6 @@ The `httplib.h` header provides a more ergonomic iterator-style API.
#include "httplib.h"
httplib::Client cli("http://localhost:8080");
cli.set_follow_location(true);
...
// Simple GET
@@ -243,23 +242,29 @@ int main() {
```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) {
httplib::Client upstream("http://backend:8080");
auto handle = upstream.open_stream("/" + req.matches[1].str());
if (!handle.is_valid()) {
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 = handle.response->status;
res.status = upstream->handle.response->status;
res.set_chunked_content_provider(
handle.response->get_header_value("Content-Type"),
[handle = std::move(handle)](size_t, httplib::DataSink& sink) mutable {
upstream->handle.response->get_header_value("Content-Type"),
[upstream](size_t, httplib::DataSink& sink) {
char buf[8192];
auto n = handle.read(buf, sizeof(buf));
auto n = upstream->handle.read(buf, sizeof(buf));
if (n > 0) {
sink.write(buf, static_cast<size_t>(n));
return true;

View File

@@ -151,9 +151,8 @@ bool is_open() const;
explicit WebSocketClient(const std::string &scheme_host_port_path,
const Headers &headers = {});
// Constructor with a client certificate for mutual TLS (wss:// only,
// requires CPPHTTPLIB_OPENSSL_SUPPORT). The certificate is ignored for
// ws:// URLs.
// Constructor with a client certificate for mutual TLS (wss:// only, SSL
// builds only). The certificate is ignored for ws:// URLs.
struct PemMemory {
const char *cert_pem;
size_t cert_pem_len;
@@ -199,7 +198,7 @@ void set_write_timeout(const std::chrono::duration<Rep, Period> &duration);
template <class Rep, class Period>
void set_connection_timeout(const std::chrono::duration<Rep, Period> &duration);
// SSL configuration (wss:// only, requires CPPHTTPLIB_OPENSSL_SUPPORT)
// SSL configuration (wss:// only, SSL builds only)
void set_ca_cert_path(const std::string &ca_cert_file_path,
const std::string &ca_cert_dir_path = std::string());
void set_ca_cert_store(tls::ca_store_t store);
@@ -473,7 +472,7 @@ ws.set_websocket_max_missed_pongs(2); // close after 2 consecutive unacked pings
The server side has the same `set_websocket_max_missed_pongs()`.
With the default ping interval of 30 seconds, `max_missed_pongs = 2` detects a dead peer within ~60 seconds. The counter is reset every time a Pong frame is received, so the mechanism only works when your code is actively calling `read()` — exactly the pattern a normal WebSocket client already uses.
With the default ping interval of 30 seconds, `max_missed_pongs = 2` detects a dead peer 60 to 90 seconds after it stops answering: the count is checked once per interval, just before the next ping goes out. A `read()` waiting on the peer at that moment returns `Fail`. The counter is reset every time a Pong frame is received, so the mechanism only works when your code is actively calling `read()`, which is exactly the pattern a normal WebSocket client already uses.
**The default is `0`**, which means "never close the connection because of missing pongs." Pings are still sent on the heartbeat interval, but their responses are not checked. On the server side a dead connection still does not linger: while a handler is inside `read()`, `CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop. A client has no such backstop — it waits forever unless you set a read timeout — so there `max_missed_pongs` is what notices an unresponsive peer at all. On either side it is also the knob for noticing one *faster* than the 5-minute fallback.

View File

@@ -33,7 +33,7 @@ Learn more in the [official documentation](https://yhirose.github.io/cpp-httplib
httplib::Server svr;
// HTTPS
httplib::SSLServer svr;
httplib::SSLServer svr("./cert.pem", "./key.pem");
svr.Get("/hi", [](const httplib::Request &, httplib::Response &res) {
res.set_content("Hello World!", "text/plain");
@@ -71,7 +71,7 @@ cpp-httplib supports multiple TLS backends through an abstraction layer:
| wolfSSL | `CPPHTTPLIB_WOLFSSL_SUPPORT` | `libwolfssl` | 5.x supported; must build with `--enable-opensslall` |
> [!NOTE]
> **Mbed TLS / wolfSSL limitation:** `get_ca_certs()` and `get_ca_names()` only reflect CA certificates loaded via `load_ca_cert_store()`. Certificates loaded through `set_ca_cert_path()` or system certificates (`load_system_certs`) are not enumerable.
> **wolfSSL limitation:** `get_ca_certs()` and `get_ca_names()` only reflect CA certificates loaded via `load_ca_cert_store()`. Certificates loaded through `set_ca_cert_path()` or system certificates (`load_system_certs`) are not enumerable.
> [!NOTE]
> **BoringSSL (best-effort):** BoringSSL builds under `CPPHTTPLIB_OPENSSL_SUPPORT` and is exercised by CI against current upstream. Because BoringSSL does not guarantee API stability, support is best-effort — breakage may occasionally land. Two known behavioral differences vs OpenSSL: (1) BoringSSL's public headers require C++14 or later, so consumers must compile accordingly; (2) hostname verification is SAN-only per RFC 6125 §6.4.4 (no CN fallback).
@@ -86,7 +86,7 @@ httplib::SSLServer svr("./cert.pem", "./key.pem");
// Client
httplib::Client cli("https://localhost:1234"); // scheme + host
httplib::SSLClient cli("localhost:1234"); // host
httplib::SSLClient cli("localhost"); // host (port 443)
httplib::SSLClient cli("localhost", 1234); // host, port
// Use your CA bundle
@@ -103,8 +103,8 @@ cli.enable_server_hostname_verification(false);
When SSL operations fail, cpp-httplib provides detailed error information through `ssl_error()` and `ssl_backend_error()`:
- `ssl_error()` - Returns the TLS-level error code (e.g., `SSL_ERROR_SSL` for OpenSSL)
- `ssl_backend_error()` - Returns the backend-specific error code (e.g., `ERR_get_error()` for OpenSSL/wolfSSL, return value for Mbed TLS)
- `ssl_error()` - Returns the TLS-level error as a backend-independent `httplib::tls::ErrorCode` value (e.g., `Fatal`, `CertVerifyFailed`), cast to `int`
- `ssl_backend_error()` - Returns the backend-specific error code (e.g., `ERR_get_error()` for OpenSSL/wolfSSL, return value for Mbed TLS). With OpenSSL, a certificate verification failure reports the verify result (`X509_V_ERR_*`) here instead
```c++
#define CPPHTTPLIB_OPENSSL_SUPPORT // or CPPHTTPLIB_MBEDTLS_SUPPORT or CPPHTTPLIB_WOLFSSL_SUPPORT
@@ -337,7 +337,7 @@ Note the following:
* The method name must be a valid HTTP method token (RFC 9110) and must be registered before `listen()` is called.
* `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH` and `PRI` cannot be registered this way. Use the dedicated methods above instead.
* A rejected registration makes `is_valid()` return `false`, and `listen()` then fails rather than starting a server with a route that would never fire.
* Static file serving and WebSocket upgrades remain `GET`/`HEAD` only.
* Static file serving remains `GET`/`HEAD` only, and WebSocket upgrades `GET` only.
* `Allow` and the WebDAV `DAV:` header are not generated automatically. Register an `Options` handler if clients need them.
### Bind a socket to multiple interfaces and any available port
@@ -518,7 +518,7 @@ svr.set_exception_handler([](const auto& req, auto& res, std::exception_ptr ep)
```
> [!CAUTION]
> if you don't provide the `catch (...)` block for a rethrown exception pointer, an uncaught exception will end up causing the server crash. Be careful!
> If you don't provide the `catch (...)` block for a rethrown exception pointer, an exception that is not a `std::exception` escapes the handler. The server keeps running, but that connection is dropped without a response, and the error logger receives `Error::UserCallbackException`.
### Pre routing handler
@@ -569,21 +569,21 @@ svr.set_pre_request_handler([](const auto& req, auto& res) {
Request received
│
├─ expect_100_continue_handler (when the request has "Expect: 100-continue")
│ └─ returns a status other than 100 → stop here
│ └─ returns a status other than 100 → go straight to post_routing_handler
│
├─ pre_routing_handler route not matched yet, body not read
│ └─ returns Handled → stop here
│ └─ returns Handled → go straight to post_routing_handler
│
├─ file_request_handler (GET/HEAD, static file serving)
├─ file_request_handler when a static file is served (GET only)
│
├─ route matching → req.matched_route is set
│
├─ pre_request_handler route matched, body NOT read yet
│ └─ returns Handled → stop here (route handler is skipped)
│ └─ returns Handled → go straight to post_routing_handler
│
├─ route handler Get/Post/...; the request body is read first
│
└─ post_routing_handler after routing completes
└─ post_routing_handler just before the response is written
On a thrown exception → exception_handler
On an error status (4xx/5xx) → error_handler
@@ -611,7 +611,7 @@ svr.set_pre_routing_handler([](const auto& req, auto& res) {
return Server::HandlerResponse::Unhandled;
});
svr.Get("/me", [](const auto& /*req*/, auto& res) {
svr.Get("/me", [](const Request& /*req*/, Response& res) {
auto* ctx = res.user_data.get<AuthContext>("auth");
if (!ctx) {
res.status = StatusCode::Unauthorized_401;
@@ -908,7 +908,7 @@ Please see [Server example](https://github.com/yhirose/cpp-httplib/blob/master/e
`ThreadPool` is used as the **default** task queue, with dynamic scaling support. By default, it maintains a base thread count of 8 or `std::thread::hardware_concurrency() - 1` (whichever is greater), and can scale up to 4x that count under load. You can change these with `CPPHTTPLIB_THREAD_POOL_COUNT` and `CPPHTTPLIB_THREAD_POOL_MAX_COUNT`.
When all threads are busy and a new task arrives, a temporary thread is spawned (up to the maximum). When a dynamic thread finishes its task and the queue is empty, or after an idle timeout, it exits automatically. The idle timeout defaults to 3 seconds, configurable via `CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT`.
When all threads are busy and a new task arrives, a temporary thread is spawned (up to the maximum). A dynamic thread exits automatically once it has waited for the idle timeout without receiving a task. The idle timeout defaults to 3 seconds, configurable via `CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT`.
If you want to set the thread counts at runtime:
@@ -1042,6 +1042,8 @@ enum class Error {
HTTPParsing,
InvalidRangeHeader,
UnsupportedContentEncoding,
WebSocketHandshake,
UserCallbackException,
};
```
@@ -1257,7 +1259,7 @@ std::string body = ...;
auto res = cli.Post(
"/stream", body.size(),
[](size_t offset, size_t length, DataSink &sink) {
[&](size_t offset, size_t length, DataSink &sink) {
sink.write(body.data() + offset, length);
return true; // return 'false' if you want to cancel the request.
},
@@ -1310,7 +1312,7 @@ cli.set_bearer_token_auth("token");
```
> [!NOTE]
> OpenSSL is required for Digest Authentication.
> A TLS backend (OpenSSL, Mbed TLS, or wolfSSL) is required for Digest Authentication.
### Proxy server support
@@ -1328,12 +1330,12 @@ cli.set_proxy_bearer_token_auth("pass");
```
> [!NOTE]
> OpenSSL is required for Digest Authentication.
> A TLS backend (OpenSSL, Mbed TLS, or wolfSSL) is required for Digest Authentication.
#### Bypass the proxy for specific hosts (`NO_PROXY`)
```cpp
cli.set_no_proxy({"internal.corp", "10.0.0.0/8", "*.dev.local"});
cli.set_no_proxy({"internal.corp", "10.0.0.0/8", ".dev.local"});
```
Each pattern is `*`, a hostname suffix, an IP literal, or a CIDR block.
@@ -1478,8 +1480,8 @@ for (auto it = req.headers.equal_range("Accept-Encoding").first;
std::cout << it->second << std::endl;
}
// get_header_value(key, id) reaches a specific one directly.
auto second = req.get_header_value("Accept-Encoding", 1); // "br"
// get_header_value(key, def, id) reaches a specific one directly.
auto second = req.get_header_value("Accept-Encoding", "", 1); // "br"
```
`Headers` matches field names case-insensitively, as before. `Params`, `FormFields`, and `FormFiles` are case-sensitive.
@@ -1489,7 +1491,7 @@ auto second = req.get_header_value("Accept-Encoding", 1); // "br"
## Payload Limit
The maximum payload body size is limited to 100MB by default for both server and client. You can change it with `set_payload_max_length()` or by defining `CPPHTTPLIB_PAYLOAD_MAX_LENGTH` at compile time. Setting it to `0` disables the limit entirely.
The maximum payload body size is limited to 100MB by default for both server and client. You can change it with `set_payload_max_length()` or by defining `CPPHTTPLIB_PAYLOAD_MAX_LENGTH` at compile time. Setting it to `0` disables the limit entirely. On the client, a request made with a content receiver is not limited unless you call `set_payload_max_length()` yourself, so that a large download can be streamed without raising the limit first.
## Compression
@@ -1498,10 +1500,15 @@ The server can apply compression to the following MIME type contents:
- all text types except text/event-stream
- image/svg+xml
- application/javascript
- application/x-javascript
- application/json
- application/ld+json
- application/xml
- application/protobuf
- application/xhtml+xml
- application/rss+xml
- application/atom+xml
- application/xslt+xml
- application/protobuf
A response that already carries `Content-Encoding` is sent as it is. A handler serving content it encoded itself, an asset compressed at build time for instance, keeps its own coding and its own bytes:
@@ -1638,7 +1645,6 @@ Process large responses without loading everything into memory.
```c++
httplib::Client cli("localhost", 8080);
cli.set_follow_location(true);
...
auto result = httplib::stream::Get(cli, "/large-file");
@@ -1718,7 +1724,7 @@ SSL is also supported via `wss://` scheme (e.g. `WebSocketClient("wss://example.
> **WebSocket extensions are not supported.** `permessage-deflate` and other RFC 6455 extensions are not implemented. If a client proposes them via `Sec-WebSocket-Extensions`, the server silently declines them in its handshake response.
> **Unresponsive-peer detection.** Heartbeat pings also serve as a liveness probe when `set_websocket_max_missed_pongs(n)` is set: if the client sends `n` consecutive pings without receiving a pong, it will close the connection. Disabled by default (`0`).
> **Unresponsive-peer detection.** Heartbeat pings also serve as a liveness probe when `set_websocket_max_missed_pongs(n)` is set: once `n` consecutive pings have gone unanswered, the connection is closed and a `read()` waiting on it fails. Both `WebSocketClient` and `Server` have the setter. Disabled by default (`0`).
See [README-websocket.md](README-websocket.md) for more details.
@@ -1727,12 +1733,14 @@ See [README-websocket.md](README-websocket.md) for more details.
`set_socket_opt` is a convenience wrapper around `setsockopt` for setting integer socket options:
```cpp
auto sock = svr.socket();
httplib::set_socket_opt(sock, IPPROTO_TCP, TCP_NODELAY, 1);
svr.set_socket_options([](socket_t sock) {
httplib::default_socket_options(sock);
httplib::set_socket_opt(sock, SOL_SOCKET, SO_KEEPALIVE, 1);
});
```
> [!TIP]
> For most use cases, prefer `set_tcp_nodelay(true)` or `set_socket_options(callback)` on the Server/Client instead of calling `set_socket_opt` directly.
> `set_socket_options` replaces the default socket options, so call `default_socket_options()` in the callback to keep them. For `TCP_NODELAY`, `set_tcp_nodelay(true)` is simpler.
## Split httplib.h into .h and .cc
@@ -1761,7 +1769,9 @@ Dockerfile for static HTTP server is available. Port number of this HTTP server
...
> docker run --rm -it -p 8080:80 -v ./docker/html:/html cpp-httplib-server
Serving HTTP on 0.0.0.0 port 80 ...
Serving HTTP on 0.0.0.0:80
Mount point: / -> ./html
Press Ctrl+C to shutdown gracefully...
192.168.65.1 - - [31/Aug/2024:21:33:56 +0000] "GET / HTTP/1.1" 200 599 "-" "curl/8.7.1"
192.168.65.1 - - [31/Aug/2024:21:34:26 +0000] "GET / HTTP/1.1" 200 599 "-" "Mozilla/5.0 ..."
192.168.65.1 - - [31/Aug/2024:21:34:26 +0000] "GET /favicon.ico HTTP/1.1" 404 152 "-" "Mozilla/5.0 ..."
@@ -1771,7 +1781,9 @@ From Docker Hub
```bash
> docker run --rm -it -p 8080:80 -v ./docker/html:/html yhirose4dockerhub/cpp-httplib-server
Serving HTTP on 0.0.0.0 port 80 ...
Serving HTTP on 0.0.0.0:80
Mount point: / -> ./html
Press Ctrl+C to shutdown gracefully...
192.168.65.1 - - [31/Aug/2024:21:33:56 +0000] "GET / HTTP/1.1" 200 599 "-" "curl/8.7.1"
192.168.65.1 - - [31/Aug/2024:21:34:26 +0000] "GET / HTTP/1.1" 200 599 "-" "Mozilla/5.0 ..."
192.168.65.1 - - [31/Aug/2024:21:34:26 +0000] "GET /favicon.ico HTTP/1.1" 404 152 "-" "Mozilla/5.0 ..."
@@ -1783,19 +1795,13 @@ NOTE
### Regular Expression Stack Overflow
> [!CAUTION]
> When using complex regex patterns in route handlers, be aware that certain patterns may cause stack overflow during pattern matching. This is a known issue with `std::regex` implementations and affects the `dispatch_request()` method.
>
> `std::regex` implementations can overflow the stack while matching a long input, even with a pattern as simple as `.*`. To keep a request from triggering this, a regex route is never applied to a path longer than `CPPHTTPLIB_REGEX_ROUTE_PATH_MAX_LENGTH` (256 by default); such a path does not match the route. Raising the limit brings the risk back with it.
>
> Path parameters are matched without `std::regex` and have no such limit, so prefer them where they fit:
>
> ```cpp
> // This pattern can cause stack overflow with large input
> svr.Get(".*", handler);
> ```
>
> Consider using simpler patterns or path parameters to avoid this issue:
>
> ```cpp
> // Safer alternatives
> svr.Get("/users/:id", handler); // Path parameters
> svr.Get(R"(/api/v\d+/.*)", handler); // More specific patterns
> svr.Get(R"(/api/v\d+/.*)", handler); // Regex route, limited to short paths
> ```
### g++