Browse Source

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.
yhirose 1 day ago
parent
commit
05f7478523
3 changed files with 66 additions and 56 deletions
  1. 16 11
      README-stream.md
  2. 4 5
      README-websocket.md
  3. 46 40
      README.md

+ 16 - 11
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>();
+    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;

+ 4 - 5
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<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.
 

+ 46 - 40
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<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.
-> 
-> ```cpp
-> // This pattern can cause stack overflow with large input
-> svr.Get(".*", handler);
-> ```
-> 
-> Consider using simpler patterns or path parameters to avoid this issue:
-> 
+> `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
-> // 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++