Fix README and docs pages that no longer match the code

Bring the READMEs and the docs pages (en and ja) in line with the
current header:

- Fix the client sample that does not compile, the Transfer-Encoding
  statement for default headers, the verifier callback comment, the
  thread pool spawn rule, the sanitize_filename description, and the
  inverted Accept-Encoding sentence.
- Describe recent behaviour: TLS errors behind Read/Write failures,
  Host validation, Connection: close for a body without length,
  content_reader rules, refused CONNECT, WebSocketClient proxies,
  CPPHTTPLIB_INTERIM_RESPONSE_MAX_COUNT, the SSE event size limit, and
  the payload limit of the Stream API.
- Complete the API references of WebSocket, WebSocketClient and the
  stream:: helpers, and the SSE reconnect rules.
- Fix cookbook pages: keychain loading on every backend, received Close
  status, a multipart sample that trusted the client's file name, an
  echo sample that sent binary as text, SNI, pre-request handler scope,
  stop() with WebSocket handlers, and the thread pool notes.
- Restore the mutex in the LLM app's Chapter 5 code.
This commit is contained in:
yhirose committed 2026-10-18 10:13:57 -04:00
1 parent 94cf1b5c9a
commit 9bfd2ddf1c
77 files changed
+378 -145

No files matched your search

@@ -50,6 +50,6 @@ httplib::Headers headers = {
auto res = cli.Get("/users", headers);
```
Per-request headers are **added** on top of the defaults. Both are sent to the server.
Per-request headers are **added** on top of the defaults, and both are sent to the server. If a per-request header has the same name as a default one, only the per-request one is sent.
> For details on Bearer token auth, see [C06. Call an API with a Bearer token](../c06-bearer-token).
@@ -33,6 +33,8 @@ auto res = cli.Get("/");
Many sites redirect HTTP traffic to HTTPS. With `set_follow_location(true)` on, this case is handled transparently — the client follows redirects even when the scheme or host changes.
When a redirect goes to another origin (a different scheme, host, or port), the client drops the `Authorization`, `Cookie`, and `Proxy-Authorization` headers, along with the credentials set with `set_basic_auth()` and the like, and doesn't present the client certificate. Other headers, default ones included, go along, and so do the timeouts, the payload limit, and the TLS verification settings.
> **Warning:** To follow redirects to HTTPS, you need to build cpp-httplib with OpenSSL (or another TLS backend). Without TLS support, redirects to HTTPS will fail.
> **Note:** Following redirects adds to the total request time. See [C12. Set timeouts](../c12-timeouts) for timeout configuration.
@@ -12,7 +12,7 @@ Sometimes you want to send a file's contents as the request body directly — no
httplib::Client cli("https://storage.example.com");
auto [size, provider] = httplib::make_file_body("backup.tar.gz");
if (size == 0) {
if (!provider) {
std::cerr << "Failed to open file" << std::endl;
return 1;
}
@@ -27,7 +27,7 @@ The `ContentProvider` reads the file in chunks, so even huge files never sit ful
## When the file can't be opened
If the file can't be opened, `make_file_body()` returns `size` as `0` and `provider` as an empty function object. Sending that would produce garbage — always check `size` first.
If the file can't be opened, `make_file_body()` returns `size` as `0` and `provider` as an empty function object. Sending that would upload an empty body — always check `provider` first. Checking `size` won't do: an empty file also gives `0`, with a working `provider`.
> **Warning:** `make_file_body()` needs to fix the Content-Length up front, so it reads the file size ahead of time. If the file size might change mid-upload, this API isn't the right fit.
@@ -12,6 +12,8 @@ The client has three kinds of timeouts, each set independently.
| Read | `set_read_timeout` | 300s | Time to wait for a single `recv` when receiving the response |
| Write | `set_write_timeout` | 5s | Time to wait for a single `send` when sending the request |
Over HTTPS, the connection timeout also bounds the whole TLS handshake. A handshake that doesn't finish in time fails with `Error::SSLConnection`.
## Basic usage
```cpp
@@ -4,7 +4,7 @@ order: 13
status: "draft"
---
The three timeouts from [C12. Set timeouts](../c12-timeouts) all apply to a single `send` or `recv` call. To cap the total time a request can take, use `set_max_timeout()`.
The three timeouts from [C12. Set timeouts](../c12-timeouts) each cover only one step: the read and write timeouts apply to a single `recv` or `send` call, and the connection timeout to connecting. To cap the total time a request can take, use `set_max_timeout()`.
## Basic usage
@@ -29,7 +29,7 @@ std::string big_payload = build_payload();
auto res = cli.Post("/api/data", big_payload, "application/json");
```
With `set_compress(true)`, the body of POST or PUT requests is compressed before sending. The encoding is the first one enabled in your build, in the order Brotli, gzip, Zstd. The server needs to handle compressed bodies too.
With `set_compress(true)`, the body of POST, PUT, or PATCH requests is compressed before sending. The encoding is the first one enabled in your build, in the order Brotli, gzip, Zstd. The server needs to handle compressed bodies too.
## Decompress the response
+2
View File
@@ -17,6 +17,8 @@ auto res = cli.Get("/users");
The request goes through the proxy. For HTTPS, the client uses the CONNECT method to tunnel through — no extra setup required.
If the proxy refuses the tunnel, the request fails with `Error::ProxyConnection`. The exception is a 407: it comes back as the response, so `res` is truthy and `res->status` is 407, while `res.error()` still returns `Error::ProxyConnection`.
## Proxy authentication
If the proxy itself requires authentication, use `set_proxy_basic_auth()` or `set_proxy_bearer_token_auth()`.
@@ -4,7 +4,7 @@ order: 17
status: "draft"
---
`cli.Get()`, `cli.Post()`, and friends return a `Result`. When the request fails — can't reach the server, times out, etc. — the result is "falsy". To get the specific reason, use `Result::error()`.
`cli.Get()`, `cli.Post()`, and friends return a `Result`. When the request fails — can't reach the server, times out, etc. — the result is "falsy", with one exception noted in the table below. To get the specific reason, use `Result::error()`.
## Basic check
@@ -27,14 +27,15 @@ Use `if (res)` to check success. On failure, `res.error()` returns a `httplib::E
| Value | Meaning |
| --- | --- |
| `Error::Connection` | Couldn't connect to the server |
| `Error::Connection` | Couldn't connect to the server, or the `Client` was built from a URL it can't use (userinfo or a backslash in the authority, a bad port). In the latter case `cli.is_valid()` is `false` |
| `Error::ConnectionTimeout` | Connection timeout (`set_connection_timeout`) |
| `Error::Read` / `Error::Write` | Error during send or receive. A timeout from `set_read_timeout` or `set_max_timeout` is also reported as `Error::Read` |
| `Error::Timeout` | A body read timed out in `stream::Get()` or `SSEClient` |
| `Error::ExceedRedirectCount` | Too many redirects |
| `Error::SSLConnection` | TLS handshake failed |
| `Error::SSLServerVerification` | Server certificate verification failed |
| `Error::Canceled` | A progress callback returned `false` |
| `Error::ProxyConnection` | The proxy refused the CONNECT tunnel. A 407 is the exception: `res` is truthy with `res->status == 407`, and `res.error()` is still `Error::ProxyConnection` |
| `Error::Canceled` | A progress callback, a `ContentReceiver`, a `ResponseHandler`, or an upload content provider returned `false` |
## Network errors vs. HTTP errors
+5 -2
View File
@@ -17,14 +17,17 @@ if (!res) {
std::cerr << "error: " << httplib::to_string(err) << std::endl;
if (err == httplib::Error::SSLConnection ||
err == httplib::Error::SSLServerVerification) {
err == httplib::Error::SSLServerVerification ||
err == httplib::Error::Read || err == httplib::Error::Write) {
std::cerr << "ssl_error: " << res.ssl_error() << std::endl;
std::cerr << "ssl_backend_error: " << res.ssl_backend_error() << std::endl;
}
}
```
`ssl_error()` is a backend-independent TLS error category: an `httplib::tls::ErrorCode` cast to `int`. `ssl_backend_error()` gives you the backend's own error value. With OpenSSL that is `ERR_get_error()` when the handshake failed, and the verify result (`X509_V_ERR_*`) when certificate verification failed.
`ssl_error()` is a backend-independent TLS error category: an `httplib::tls::ErrorCode` cast to `int`. `ssl_backend_error()` gives you the backend's own error value. With OpenSSL that is `ERR_get_error()` when the handshake or a later read or write failed, and the verify result (`X509_V_ERR_*`) when certificate verification failed.
A request that fails after the handshake reports `Error::Read` or `Error::Write`. Even then, `ssl_error()` and `ssl_backend_error()` carry the TLS error behind it. For example, if the server requires a client certificate you didn't send, OpenSSL's error reads "tlsv13 alert certificate required". Both values are reset for each request and are `0` when no TLS error was involved.
## Format OpenSSL errors as strings
@@ -35,7 +35,7 @@ cli.set_error_logger([](const httplib::Error &err, const httplib::Request *req)
});
```
The second argument `req` can be null — it happens when the failure occurred before the request was built. Always null-check before dereferencing.
The second argument `req` can be null — it happens when the error comes from setting up the connection, such as the TLS handshake, loading CA certificates, or opening a tunnel through a proxy. Always null-check before dereferencing.
## Use both together
+5 -1
View File
@@ -55,7 +55,9 @@ sse.on_error([](httplib::Error err) {
});
```
Hook into connection open and error events. Even when the error handler fires, `SSEClient` keeps trying to reconnect in the background. The exceptions are a 204, 403, or 404 response, after which it stops.
Hook into connection open and error events. Even when the error handler fires, `SSEClient` keeps trying to reconnect in the background. It stops after a 204, 403, or 404 response, or once the attempts set with `set_max_reconnect_attempts()` run out.
It also stops when an event, or a line still waiting for its end, grows past `CPPHTTPLIB_SSE_MAX_EVENT_LENGTH` (16MB by default, 0 for no limit). The error handler then gets `Error::ExceedMaxPayloadSize`. The client's `set_payload_max_length()` doesn't apply to the stream as a whole, since an event stream has no end.
## Run asynchronously
@@ -88,6 +90,8 @@ If the server sends a `retry:` field, that takes precedence.
`SSEClient` tracks the `id` of each received event internally and sends it back as `Last-Event-ID` on reconnect. As long as the server sends events with `id:`, this all works automatically.
An empty `id:` clears the stored id. An `id:` that can't be sent as a header value (one with NUL or other control characters, or with leading or trailing whitespace) is ignored, and the previous id stays.
```cpp
std::cout << "last id: " << sse.last_event_id() << std::endl;
```
@@ -44,6 +44,8 @@ svr.Get(R"(/users/(\d+))", [](const httplib::Request &req, httplib::Response &re
Parentheses in the pattern become captures in `req.matches`. `req.matches[0]` is the full match; `req.matches[1]` onward are the captures.
A regex route isn't applied to a path longer than `CPPHTTPLIB_REGEX_ROUTE_PATH_MAX_LENGTH` (256 by default); such a path is treated as no match. This keeps `std::regex` from overflowing the stack on long input. If your routes need longer paths, define the macro with a larger value.
## Which to use
- For plain IDs or slugs, `:name` is enough — readable, and the shape is obvious
@@ -10,12 +10,19 @@ To serve static files like HTML, CSS, and images, use `set_mount_point()`. Just
```cpp
httplib::Server svr;
svr.set_mount_point("/", "./public");
if (!svr.set_mount_point("/", "./public")) {
// ./public doesn't exist
return 1;
}
svr.listen("0.0.0.0", 8080);
```
`set_mount_point()` returns `false` when the directory doesn't exist, so check the return value.
`./public/index.html` is now reachable at `http://localhost:8080/index.html`, and `./public/css/style.css` at `http://localhost:8080/css/style.css`. The directory layout maps directly to URLs.
A request for a directory serves its `index.html`, and one without the trailing slash (`/docs`) is redirected (301) to `/docs/`. A symlink that resolves outside the mounted directory gets a 403.
## Multiple mount points
You can register more than one mount point.
@@ -48,22 +48,30 @@ svr.Post("/upload",
const httplib::ContentReader &content_reader) {
std::ofstream ofs;
content_reader(
auto ok = content_reader(
[&](const httplib::FormData &file) {
if (!file.filename.empty()) {
ofs.open("uploads/" + file.filename, std::ios::binary);
}
return static_cast<bool>(ofs);
if (ofs.is_open()) { ofs.close(); } // done with the previous file
if (file.filename.empty()) { return true; } // a plain field, skip it
auto name = httplib::sanitize_filename(file.filename);
if (name.empty()) { return false; } // reject an unsafe name
ofs.open("uploads/" + name, std::ios::binary);
return ofs.is_open();
},
[&](const char *data, size_t len) {
if (!ofs.is_open()) { return true; } // not part of a file
ofs.write(data, len);
return static_cast<bool>(ofs);
});
if (!ok) { return; } // the status is already set
res.set_content("uploaded", "text/plain");
});
```
The header callback runs at the start of every part, so it closes the previous file before opening the next one. `file.filename` is whatever the client sent, so always pass it through `httplib::sanitize_filename()`. An empty result means the name is unsafe.
Only a small chunk sits in memory at any moment, so gigabyte-scale files are no problem.
## Count the parts yourself
@@ -16,7 +16,7 @@ svr.set_pre_routing_handler(
});
```
The pre-routing handler runs **before routing**. It catches every request — including ones that don't match any handler.
The pre-routing handler runs **before routing**. It catches every request — including ones that don't match any handler. The exception is a request rejected while its request line and headers are parsed (400 for a malformed request, 414 for an overlong URI, 416 for a malformed `Range`); the server answers those before this handler runs.
The `HandlerResponse` return value is key:
@@ -38,6 +38,8 @@ svr.Options("/.*", [](const auto &req, auto &res) {
});
```
`/.*` is a regex route, so it isn't applied to paths longer than `CPPHTTPLIB_REGEX_ROUTE_PATH_MAX_LENGTH` (256 by default). If your paths can be longer, handle `OPTIONS` in the pre-routing handler instead.
## Bundle your security headers
Manage browser security headers in one spot.
@@ -17,6 +17,8 @@ In a pre-request handler, `req.matched_route` holds the **pattern string** that
Because the body has not been read when the pre-request handler runs, you can reject a request — for example on a failed auth check — without consuming a (potentially large) request body. Note that this also means `req.body` and form fields parsed from the body are not available here; inspect headers, the path, query parameters, or `req.matched_route` instead.
The pre-request handler runs only for requests that match a route. Files served by `set_mount_point()` are answered before route matching, so guard static files in the pre-routing handler.
## Switch auth per route
```cpp
@@ -39,6 +41,8 @@ svr.set_pre_request_handler(
The pre-request handler also runs for routes registered with `svr.WebSocket()`. It is called before the `101 Switching Protocols` response, so returning `Handled` sends your HTTP response (such as a 403) and the connection is never upgraded.
The server accepts a handshake from any `Origin`. A browser sends its cookies even with a handshake that another site starts, so if cookies authenticate the connection, also check `req.get_header_value("Origin")` against your own origins here.
## Return values
Same as pre-routing — return `HandlerResponse`.
@@ -4,7 +4,7 @@ order: 32
status: "draft"
---
To customize the response for 4xx or 5xx errors, use `set_error_handler()`. You can replace the plain default error page with your own HTML or JSON.
To customize the response for 4xx or 5xx errors, use `set_error_handler()`. By default, an error response has an empty body; an error handler lets you fill it with your own HTML or JSON.
## Basic usage
@@ -4,7 +4,7 @@ order: 34
status: "draft"
---
To log the requests the server receives and the responses it returns, use `Server::set_logger()`. The callback fires once per completed request, making it the foundation for access logs and metrics collection.
To log the requests the server receives and the responses it returns, use `Server::set_logger()`. The callback fires once per request, right before the response is written, making it the foundation for access logs and metrics collection.
## Basic usage
@@ -18,6 +18,8 @@ svr.set_logger([](const httplib::Request &req, const httplib::Response &res) {
The log callback receives both the `Request` and the `Response`. You can grab the method, path, status, client IP, headers, body — whatever you need.
`req.path` is percent-decoded, so it may contain control characters such as CR/LF. Escape it before writing it to a log file, or a crafted request can forge log lines.
## Access-log style format
Here's an Apache/Nginx-ish access log format.
@@ -38,6 +40,8 @@ svr.set_logger([](const auto &req, const auto &res) {
});
```
For a response sent through a content provider, such as a file served from a mount point, `res.body` is empty here, so the size shows `0B`.
## Measure request time
To include request duration in the log, stash a start timestamp in `res.user_data` from a pre-routing handler, then subtract in the logger.
@@ -59,6 +63,8 @@ svr.set_logger([](const httplib::Request &req, const httplib::Response &res) {
});
```
Since the logger runs before the response is written, the time doesn't include sending the body.
For more on `user_data`, see [S12. Pass data between handlers with `res.user_data`](../s12-user-data).
> **Note:** The logger runs synchronously on the same thread as request processing. Heavy work inside it hurts throughput — push it to a queue and process asynchronously if you need anything expensive.
> **Note:** The logger runs synchronously on the same thread as request processing, and all logger calls on a server share one lock. Heavy work inside it hurts throughput — push it to a queue and process asynchronously if you need anything expensive.
@@ -55,3 +55,5 @@ int main() {
When you call `stop()`, new connections are refused, but handlers that are already running are **allowed to finish**. A response still being sent by a content provider (a streaming response, for example) is cut short, though. Once all workers drain, `listen()` returns. That's what makes it graceful.
> **Warning:** There's a wait between calling `stop()` and `listen()` returning — it's the time in-flight requests take to finish. To enforce a timeout, you'll need to add your own shutdown timer in application code.
> **Note:** `stop()` doesn't interrupt WebSocket handlers. A handler looping on `read()` keeps running as long as its peer stays connected, and `listen()` doesn't return until it ends. Keep track of open WebSocket connections and `close()` them on shutdown, e.g. with `CloseStatus::GoingAway`.
+1 -1
View File
@@ -52,6 +52,6 @@ Set `set_keep_alive_max_count(1)` and every request gets its own connection. Mos
## Relationship with the thread pool
A Keep-Alive connection holds a worker thread for its entire lifetime. If `connections × concurrent requests` exceeds the thread pool size, new requests wait. For thread counts, see [S21. Configure the thread pool](../s21-thread-pool).
A Keep-Alive connection holds a worker thread for its entire lifetime. The pool adds threads under load, up to its maximum thread count (4× the base count by default). If open connections outnumber that maximum, new connections wait in the queue until one closes. For thread counts, see [S21. Configure the thread pool](../s21-thread-pool).
> **Note:** For the client side, see [C14. Understand connection reuse and Keep-Alive behavior](../c14-keep-alive). Even when the server closes the connection on timeout, the client reconnects automatically.
@@ -66,4 +66,4 @@ You can set the defaults with macros if you want compile-time configuration.
#include <httplib.h>
```
> **Note:** A WebSocket connection holds a worker thread for its entire lifetime. For lots of simultaneous WebSocket connections, enable dynamic scaling (e.g. `ThreadPool(8, 64)`).
> **Note:** A WebSocket connection holds a worker thread for its entire lifetime. For lots of simultaneous WebSocket connections, raise the maximum thread count (e.g. `ThreadPool(8, 64)`).
@@ -46,4 +46,4 @@ Certificate verification control, standing up an SSLServer, reading the peer cer
- [T03. Start an SSL/TLS server](../t03-ssl-server)
- [T05. Access the peer certificate on the server](../t05-peer-cert)
> **Note:** On macOS with an OpenSSL-family backend, cpp-httplib automatically loads root certificates from the system keychain (via `CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN`, on by default). To disable this, define `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES`.
> **Note:** On macOS, with any of the three backends, cpp-httplib automatically loads root certificates from the system keychain (via `CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN`, on by default). To disable this, define `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES`.
@@ -19,6 +19,8 @@ auto res = cli.Get("/");
The first argument is the CA certificate file; the second is an optional CA directory. With the OpenSSL backend, you can also pass an `X509_STORE*` directly via `set_ca_cert_store()`.
Once you set a custom CA, the client trusts only that CA and skips the OS store. To trust both, also call `cli.enable_system_ca(true)`. CA certificates are loaded once, on the client's first connection, so make these settings before the first request.
## Disable certificate verification (not recommended)
For development servers or self-signed certificates, you can skip verification entirely.
@@ -46,7 +48,7 @@ The certificate itself is still validated, so this is safer than fully disabling
## Use the OS cert store as-is
On most Linux distributions, root certificates live in a single file like `/etc/ssl/certs/ca-certificates.crt`. cpp-httplib reads the OS default store at startup, so for most servers you don't need to configure anything.
On most Linux distributions, root certificates live in a single file like `/etc/ssl/certs/ca-certificates.crt`. cpp-httplib loads the OS default store on the client's first connection, so for most servers you don't need to configure anything.
> The same APIs work on the mbedTLS and wolfSSL backends. For choosing between backends, see [T01. Choosing between OpenSSL, mbedTLS, and wolfSSL](../t01-tls-backends).
+2 -2
View File
@@ -44,7 +44,7 @@ for (const auto &san : sans) {
}
```
There's also a helper to check if a hostname is covered by the SAN list:
There's also a helper that checks whether the certificate covers a hostname. It follows the rules of TLS hostname verification: an IP address is matched against the IP address SANs, a name against the DNS SANs, and the CN is used only when there's no DNS SAN.
```cpp
if (cert.check_hostname("alice.corp.example.com")) {
@@ -81,7 +81,7 @@ Combined with a pre-request handler, you can keep all authorization logic in one
## SNI (Server Name Indication)
cpp-httplib handles SNI automatically. If one server hosts multiple domains, SNI is used under the hood — but normally handlers don't need to care.
Clients send SNI automatically. On the server, `req.sni()` returns the name the client asked for. `SSLServer` presents the same certificate whatever that name is, though, so to host several domains on one server, use a certificate that covers all of them. To choose a certificate per name, hook into the TLS backend yourself — with OpenSSL, for example, set a servername callback through the `SSLServer` constructor that takes a context setup callback.
> **Warning:** `req.peer_cert()` only returns a meaningful value when mTLS is enabled and the client actually presented a certificate. For plain TLS, you get an empty `PeerCert`. Always do the `bool` check before using it.
@@ -21,7 +21,12 @@ int main() {
if (result == httplib::ws::ReadResult::Fail) {
break;
}
ws.send(msg); // echo back what we received
// echo back what we received, in the same frame type
if (result == httplib::ws::ReadResult::Binary) {
ws.send(msg.data(), msg.size());
} else {
ws.send(msg);
}
}
});
@@ -29,7 +34,7 @@ int main() {
}
```
Register a WebSocket handler with `svr.WebSocket()`. By the time the handler runs, the WebSocket handshake is already complete. Inside the loop, just `ws.read()` and `ws.send()` to get a working echo.
Register a WebSocket handler with `svr.WebSocket()`. By the time the handler runs, the WebSocket handshake is already complete. Inside the loop, just `ws.read()` and `ws.send()` to get a working echo. A binary message goes back through the `data()` + `size()` overload so it stays binary (more on that below).
The `read()` return value is a `ReadResult` enum:
@@ -76,7 +81,7 @@ The `std::string` overload sends as **text**; the `const char*` + size overload
## Thread pool implications
A WebSocket handler holds its worker thread for the entire life of the connection — one connection per thread. For many concurrent clients, configure a dynamic thread pool.
A WebSocket handler holds its worker thread for the entire life of the connection — one connection per thread. The default pool already grows under load, up to 4× its base thread count. For many concurrent clients, raise that maximum.
```cpp
svr.new_task_queue = [] {
@@ -43,7 +43,7 @@ The default interval is set by the build-time macro `CPPHTTPLIB_WEBSOCKET_PING_I
## What about Pong?
The WebSocket protocol requires that Ping frames are answered with Pong frames. cpp-httplib responds to Pings automatically — you don't need to think about it in application code.
The WebSocket protocol requires that Ping frames are answered with Pong frames. cpp-httplib answers Pings automatically inside `read()` — as long as your code keeps calling `read()`, you don't need to think about it.
## Picking an interval
@@ -67,7 +67,7 @@ To notify in-flight clients that the server is going down, use `GoingAway`.
ws.close(httplib::ws::CloseStatus::GoingAway, "server restarting");
```
The client can inspect that status and decide whether to reconnect.
On the client, `read()` just returns `Fail`; the status and reason it received aren't exposed. If the client needs to tell a restart from other closures, send it a message first (say, `ws.send("restarting")`), then call `close()`.
## Example: a tiny chat with quit
@@ -85,4 +85,4 @@ if (cli.read(buf) == httplib::ws::ReadResult::Binary) {
You can mix text and binary in the same connection. A common pattern: JSON for control messages, binary for the actual data — you get efficient handling of metadata and payload both.
> **Note:** WebSocket frames don't have an infinite size limit. For very large data, chunk it in your application code. cpp-httplib can handle a big frame in one shot, but it does load it all into memory at once.
> **Note:** An incoming message can be at most `CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH` (16MB by default, 0 for no limit). A larger one makes `read()` return `Fail` and ends the connection. Within that limit, a message is held in memory whole, so for very large data, chunk it in your application code.
@@ -26,6 +26,8 @@ if (ws.connect()) {
To disable certificate verification entirely, use `enable_server_certificate_verification(false)`. For details on that behavior, see [T02. Control SSL Certificate Verification](../t02-cert-verification).
`WebSocketClient` also has `set_server_certificate_verifier()` and `set_session_verifier()` for custom checks, plus the proxy setters (`set_proxy()`, `set_proxy_basic_auth()`, `set_proxy_bearer_token_auth()`, `set_proxy_digest_auth()`, `set_no_proxy()`). CA certificates are loaded on the first `connect()`, so set them before that.
## Presenting a client certificate (mTLS)
`ws::WebSocketClient` has a constructor overload that takes a `PemMemory` struct, letting `wss://` connections present a client certificate.
+1 -1
View File
@@ -9,7 +9,7 @@ When you need a quick HTTP server or client in C++, you want something that just
The API uses a lambda-based design that feels natural. It runs anywhere you have a C++11 or later compiler. Windows, macOS, Linux — use whatever environment you already have.
HTTPS works too. Just link OpenSSL or mbedTLS, and both server and client gain TLS support. Content-Encoding (gzip, Brotli, etc.), file uploads, and other features you actually need in real-world development are all included. WebSocket is also supported.
HTTPS works too. Just link OpenSSL, Mbed TLS, or wolfSSL, and both server and client gain TLS support. Content-Encoding (gzip, Brotli, etc.), file uploads, and other features you actually need in real-world development are all included. WebSocket is also supported.
Under the hood, it uses blocking I/O with a thread pool. It's not built for handling massive numbers of simultaneous connections. But for API servers, embedded HTTP in tools, mock servers for testing, and many other use cases, it delivers solid performance.
@@ -76,7 +76,7 @@ Defining `CPPHTTPLIB_OPENSSL_SUPPORT` enables `httplib::Client("https://...")` t
## 4.2 Defining the Model List
Let's define the list of models that the app can handle. Here are four models we've verified for translation tasks.
Let's define the list of models that the app can handle. Here are three models we've verified for translation tasks.
```cpp
struct ModelInfo {
+24 -16
View File
@@ -875,6 +875,7 @@ The only server-side change is the single `set_mount_point` line. Add it before
#include <filesystem>
#include <fstream>
#include <iostream>
#include <mutex>
using json = nlohmann::json;
@@ -934,11 +935,11 @@ std::filesystem::path get_models_dir() {
// Model download
// -------------------------------------------------------------------------
// Abort the download if progress_cb returns false
// If progress_cb returns false, the download is aborted
bool download_model(const ModelInfo &model,
std::function<bool(int)> progress_cb) {
httplib::Client cli("https://huggingface.co");
cli.set_follow_location(true); // Hugging Face redirects to CDN
cli.set_follow_location(true); // Hugging Face redirects to a CDN
auto url = "/" + model.repo + "/resolve/main/" + model.filename;
auto path = get_models_dir() / model.filename;
@@ -948,12 +949,12 @@ bool download_model(const ModelInfo &model,
if (!ofs) { return false; }
auto res = cli.Get(url,
// content_receiver: receive chunks and write to file
// content_receiver: receive data chunk by chunk and write to file
[&](const char *data, size_t len) {
ofs.write(data, len);
return ofs.good();
},
// progress: report download progress (return false to abort)
// progress: report download progress (returning false aborts)
[&, last_pct = -1](size_t current, size_t total) mutable {
int pct = total ? (int)(current * 100 / total) : 0;
if (pct == last_pct) return true; // Skip if same value
@@ -987,11 +988,11 @@ void signal_handler(int sig) {
}
int main() {
// Create model storage directory
// Create the model storage directory
auto models_dir = get_models_dir();
std::filesystem::create_directories(models_dir);
// Auto-download default model if not present
// Automatically download the default model if not yet present
std::string selected_model = MODELS[0].filename;
auto path = models_dir / selected_model;
if (!std::filesystem::exists(path)) {
@@ -1006,6 +1007,7 @@ int main() {
std::cout << std::endl;
}
auto llm = llamalib::Llama{path};
std::mutex llm_mutex; // Protect access during model switching
svr.set_logger([](const auto &req, const auto &res) {
std::cout << req.method << " " << req.path << " -> " << res.status
@@ -1020,6 +1022,7 @@ int main() {
svr.Post("/translate",
[&](const httplib::Request &req, httplib::Response &res) {
// JSON parsing and validation (see Chapter 2 for details)
auto input = json::parse(req.body, nullptr, false);
if (input.is_discarded()) {
res.status = 400;
@@ -1043,6 +1046,7 @@ int main() {
". Output only the translation, nothing else.\n\n" + text;
try {
std::lock_guard<std::mutex> lock(llm_mutex);
auto translation = llm.chat(prompt);
res.set_content(json{{"translation", translation}}.dump(),
"application/json");
@@ -1052,7 +1056,7 @@ int main() {
}
});
// --- SSE streaming translation (Chapter 3) --------------------------------
// --- SSE streaming translation (Chapter 3) -------------------------------
svr.Post("/translate/stream",
[&](const httplib::Request &req, httplib::Response &res) {
@@ -1081,6 +1085,7 @@ int main() {
res.set_chunked_content_provider(
"text/event-stream",
[&, prompt](size_t, httplib::DataSink &sink) {
std::lock_guard<std::mutex> lock(llm_mutex);
try {
llm.chat(prompt, [&](std::string_view token) {
sink.os << "data: "
@@ -1098,7 +1103,7 @@ int main() {
});
});
// --- Model list (Chapter 4) -----------------------------------------------
// --- Model list (Chapter 4) ----------------------------------------------
svr.Get("/models",
[&](const httplib::Request &, httplib::Response &res) {
@@ -1117,7 +1122,7 @@ int main() {
res.set_content(json{{"models", arr}}.dump(), "application/json");
});
// --- Model selection (Chapter 4) ------------------------------------------
// --- Model selection (Chapter 4) -----------------------------------------
svr.Post("/models/select",
[&](const httplib::Request &req, httplib::Response &res) {
@@ -1143,16 +1148,16 @@ int main() {
const auto &model = *it;
// Always respond with SSE (same format whether downloaded or not)
// Always respond with SSE (same format whether already downloaded or not)
res.set_chunked_content_provider(
"text/event-stream",
[&, model](size_t, httplib::DataSink &sink) {
// SSE event send helper
// SSE event sending helper
auto send = [&](const json &event) {
sink.os << "data: " << event.dump() << "\n\n";
};
// Download if not yet downloaded (report progress via SSE)
// Download if not yet present (report progress via SSE)
auto path = get_models_dir() / model.filename;
if (!std::filesystem::exists(path)) {
bool ok = download_model(model, [&](int pct) {
@@ -1168,8 +1173,11 @@ int main() {
// Load and switch to the model
send({{"status", "loading"}});
llm = llamalib::Llama{path};
selected_model = model.filename;
{
std::lock_guard<std::mutex> lock(llm_mutex);
llm = llamalib::Llama{path};
selected_model = model.filename;
}
send({{"status", "ready"}});
sink.done();
@@ -1177,11 +1185,11 @@ int main() {
});
});
// --- Static file serving (Chapter 5) --------------------------------------
// --- Static file serving (Chapter 5) -------------------------------------
svr.set_mount_point("/", "./public");
// Allow graceful shutdown via `Ctrl+C` (`SIGINT`) or `kill` (`SIGTERM`)
// Allow the server to be stopped with `Ctrl+C` (`SIGINT`) or `kill` (`SIGTERM`)
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
@@ -675,9 +675,10 @@ int main() {
To summarize the changes from Chapter 5:
- `#include <csignal>` replaced with `#include <thread>`, `<cpp-embedlib-httplib.h>`, `"WebAssets.h"`, `"webview/webview.h"`
- Removed the `signal_handler` function
- Removed the `signal_handler` function and moved `svr` into `main()`
- `svr.set_mount_point("/", "./public")` replaced with `httplib::mount(svr, Web::FS)`
- `svr.listen("127.0.0.1", 8080)` replaced with `bind_to_any_port` + `listen_after_bind` + WebView event loop
- Added `setup_macos_edit_menu()` and its `<objc/objc-runtime.h>` include (macOS only)
Not a single line of handler code has changed. The REST API, SSE streaming, and model management built through Chapter 5 all work as-is.
@@ -59,7 +59,7 @@ Compatibility with existing tools and libraries is a big design decision. We des
## 7.3 Concurrent Request Handling
Our server processes requests one at a time. If another request arrives while a translation is in progress, it waits until the previous inference finishes. This is fine for a desktop app used by one person, but it becomes a problem for a server shared by multiple users.
Our server runs inference one request at a time. If another request arrives while a translation is in progress, it waits until the previous inference finishes. This is fine for a desktop app used by one person, but it becomes a problem for a server shared by multiple users.
`llama-server` handles concurrent requests through a mechanism called **slots**.
@@ -67,7 +67,7 @@ Our server processes requests one at a time. If another request arrives while a
The key point is that tokens from each slot are not inferred **one by one in sequence**, but rather **all at once in a single batch**. GPUs excel at parallel processing, so processing two users simultaneously takes almost the same time as processing one. This is called "continuous batching."
In our server, cpp-httplib's thread pool assigns one thread per request, but the inference itself runs single-threaded inside `llm.chat()`. `llama-server` consolidates this inference step into a shared batch processing loop.
In our server, cpp-httplib's thread pool serves each connection on its own worker thread, so requests on different connections are handled concurrently. Inference, however, is serialized by `llm_mutex`, so only one `llm.chat()` runs at a time. `llama-server` consolidates this inference step into a shared batch processing loop.
## 7.4 Differences in SSE Format
+3 -1
View File
@@ -21,7 +21,9 @@ if (result) {
}
```
You can also pass a `content_receiver` callback to `Get()`. This approach works with Keep-Alive.
Note that `stream::Get()` reads no more of the body than the client's `set_payload_max_length()` allows, which is 100MB by default. For larger downloads, raise it, or set it to 0 for no limit.
You can also pass a `content_receiver` callback to `Get()`. This approach works with Keep-Alive, and it has no size limit unless you set one with `set_payload_max_length()`.
```cpp
httplib::Client cli("http://localhost:8080");