From 05f74785235214988d9fba14be0971eb6aa9aef8 Mon Sep 17 00:00:00 2001 From: yhirose Date: Thu, 8 Oct 2026 00:40:16 -0400 Subject: [PATCH] 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() 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. --- README-stream.md | 27 ++++++++------ README-websocket.md | 9 +++-- README.md | 86 ++++++++++++++++++++++++--------------------- 3 files changed, 66 insertions(+), 56 deletions(-) diff --git a/README-stream.md b/README-stream.md index b7f4162..85cd966 100644 --- a/README-stream.md +++ b/README-stream.md @@ -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->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(n)); return true; diff --git a/README-websocket.md b/README-websocket.md index 7a3c892..06d7784 100644 --- a/README-websocket.md +++ b/README-websocket.md @@ -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 &duration); template void set_connection_timeout(const std::chrono::duration &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. diff --git a/README.md b/README.md index 7e07e30..afd9d55 100644 --- a/README.md +++ b/README.md @@ -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("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++