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");
@@ -50,6 +50,6 @@ httplib::Headers headers = {
auto res = cli.Get("/users", headers);
```
リクエスト単位で渡したヘッダーはデフォルトヘッダーに**追加**されます。両方がサーバーに送られます。
リクエスト単位で渡したヘッダーはデフォルトヘッダーに**追加**され、両方がサーバーに送られます。デフォルトヘッダーと同じ名前のヘッダーを渡した場合は、リクエスト単位のものだけが送られます。
> Bearerトークンを使った認証の詳細は[C06. BearerトークンでAPIを呼ぶ](../c06-bearer-token)を参照してください。
@@ -33,6 +33,8 @@ auto res = cli.Get("/");
多くのサイトはHTTPアクセスをHTTPSへリダイレクトします。`set_follow_location(true)`を有効にしておけば、こうしたケースも透過的に扱えます。スキームやホストが変わっても自動で追従します。
リダイレクト先が別のオリジン(スキーム、ホスト、ポートのどれかが違う)の場合、`Authorization`、`Cookie`、`Proxy-Authorization`ヘッダーは外され、`set_basic_auth()`などで設定した認証情報も送られません。クライアント証明書も提示しません。それ以外のヘッダーはデフォルトヘッダーも含めてそのまま送られ、タイムアウト、ペイロードの上限、TLSの検証設定も引き継がれます。
> **Warning:** HTTPSへのリダイレクトを追従するには、cpp-httplibをOpenSSL(または他のTLSバックエンド)付きでビルドしておく必要があります。TLSサポートがないと、HTTPSへのリダイレクトは失敗します。
> **Note:** リダイレクトを追従すると、リクエストの実行時間は伸びます。タイムアウトの設定は[C12. タイムアウトを設定する](../c12-timeouts)を参照してください。
@@ -12,7 +12,7 @@ status: "draft"
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 @@ auto res = cli.Put("/bucket/backup.tar.gz", size,
## ファイルが開けなかったとき
`make_file_body()`は開けなかった場合、`size`を`0`、`provider`を空の関数オブジェクトとして返します。そのまま送信するとおかしな結果になるので、必ず`size`をチェックしてください。
`make_file_body()`は開けなかった場合、`size`を`0`、`provider`を空の関数オブジェクトとして返します。そのまま送信すると空のボディが送られてしまうので、必ず`provider`をチェックしてください。`size`では判定できません。空のファイルでも`size`は`0`になり、そのときの`provider`は正しく使えるからです。
> **Warning:** `make_file_body()`はContent-Lengthを最初に確定させる必要があるため、ファイルサイズをあらかじめ取得します。送信中にファイルサイズが変わる可能性がある場合は、このAPIには向きません。
@@ -12,6 +12,8 @@ status: "draft"
| 読み取りタイムアウト | `set_read_timeout` | 300秒 | レスポンスを受信する際の1回の`recv`待ち時間 |
| 書き込みタイムアウト | `set_write_timeout` | 5秒 | リクエストを送信する際の1回の`send`待ち時間 |
HTTPSでは、接続タイムアウトがTLSハンドシェイク全体の制限時間にもなります。時間内にハンドシェイクが終わらないと、`Error::SSLConnection`で失敗します。
## 基本の使い方
```cpp
@@ -4,7 +4,7 @@ order: 13
status: "draft"
---
[C12. タイムアウトを設定する](../c12-timeouts)で紹介した3種類のタイムアウトは、いずれも「1回の`send`や`recv`」に対するものです。リクエスト全体の所要時間に上限を設けたい場合は、`set_max_timeout()`を使います。
[C12. タイムアウトを設定する](../c12-timeouts)で紹介した3種類のタイムアウトは、どれも一部の待ち時間にしか効きません。読み取りと書き込みのタイムアウトは「1回の`recv`や`send`」に、接続タイムアウトは接続の確立に対するものです。リクエスト全体の所要時間に上限を設けたい場合は、`set_max_timeout()`を使います。
## 基本の使い方
@@ -29,7 +29,7 @@ std::string big_payload = build_payload();
auto res = cli.Post("/api/data", big_payload, "application/json");
```
`set_compress(true)`を呼んでおくと、POSTやPUTのリクエストボディが圧縮されて送信されます。圧縮方式は、ビルドで有効にしたもののうち、Brotli、gzip、Zstdの順で最初に当てはまるものです。サーバー側もその方式に対応している必要があります。
`set_compress(true)`を呼んでおくと、POST、PUT、PATCHのリクエストボディが圧縮されて送信されます。圧縮方式は、ビルドで有効にしたもののうち、Brotli、gzip、Zstdの順で最初に当てはまるものです。サーバー側もその方式に対応している必要があります。
## レスポンスを解凍する
+2
View File
@@ -17,6 +17,8 @@ auto res = cli.Get("/users");
プロキシ経由でリクエストが送られます。HTTPSの場合はCONNECTメソッドでトンネルが張られるので、cpp-httplib側で特別な設定は要りません。
プロキシがトンネルを拒否すると、リクエストは`Error::ProxyConnection`で失敗します。ただし407だけは例外で、レスポンスとして返ってきます。`res`はtruthyで`res->status`は407ですが、`res.error()`は`Error::ProxyConnection`のままです。
## プロキシに認証を設定する
プロキシ自体が認証を要求する場合は、`set_proxy_basic_auth()`や`set_proxy_bearer_token_auth()`を使います。
@@ -4,7 +4,7 @@ order: 17
status: "draft"
---
`cli.Get()`や`cli.Post()`は`Result`型を返します。リクエストが失敗したとき(サーバーに到達できなかった、タイムアウトしたなど)、返り値は「falsy」になります。詳しい原因を知りたい場合は`Result::error()`を使います。
`cli.Get()`や`cli.Post()`は`Result`型を返します。リクエストが失敗したとき(サーバーに到達できなかった、タイムアウトしたなど)、返り値は「falsy」になります(例外が1つあり、下の表で説明します)。詳しい原因を知りたい場合は`Result::error()`を使います。
## 基本の判定
@@ -27,14 +27,15 @@ if (res) {
| 値 | 意味 |
| --- | --- |
| `Error::Connection` | サーバーに接続できなかった |
| `Error::Connection` | サーバーに接続できなかった。または、使えないURL(authorityにuserinfoやバックスラッシュがある、ポートが不正など)から`Client`を作った。後者の場合は`cli.is_valid()`が`false`になる |
| `Error::ConnectionTimeout` | 接続タイムアウト(`set_connection_timeout`) |
| `Error::Read` / `Error::Write` | 送受信中のエラー。`set_read_timeout`や`set_max_timeout`によるタイムアウトも`Error::Read`になる |
| `Error::Timeout` | `stream::Get()`や`SSEClient`で、ボディの読み取りがタイムアウトした |
| `Error::ExceedRedirectCount` | リダイレクト回数が上限を超えた |
| `Error::SSLConnection` | TLSハンドシェイクに失敗 |
| `Error::SSLServerVerification` | サーバー証明書の検証に失敗 |
| `Error::Canceled` | 進捗コールバックから`false`が返された |
| `Error::ProxyConnection` | プロキシがCONNECTのトンネルを拒否した。ただし407は例外で、`res`はtruthyかつ`res->status == 407`になり、`res.error()`は`Error::ProxyConnection`のまま |
| `Error::Canceled` | 進捗コールバック、`ContentReceiver`、`ResponseHandler`、アップロード用のコンテンツプロバイダのいずれかから`false`が返された |
## ステータスコードとの使い分け
+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()`は、どのTLSバックエンドでも共通のエラー種別(`httplib::tls::ErrorCode`)を`int`で返します。`ssl_backend_error()`は、バックエンドが返したエラー値そのものです。OpenSSLなら、ハンドシェイクの失敗では`ERR_get_error()`の値が、証明書検証の失敗では検証結果のコード(`X509_V_ERR_*`)が入ります。
`ssl_error()`は、どのTLSバックエンドでも共通のエラー種別(`httplib::tls::ErrorCode`)を`int`で返します。`ssl_backend_error()`は、バックエンドが返したエラー値そのものです。OpenSSLなら、ハンドシェイクやその後の読み書きの失敗では`ERR_get_error()`の値が、証明書検証の失敗では検証結果のコード(`X509_V_ERR_*`)が入ります。
ハンドシェイクのあとで失敗したリクエストは、`Error::Read`や`Error::Write`を返します。その場合も、`ssl_error()`と`ssl_backend_error()`には原因となったTLSのエラーが入ります。たとえば、サーバーが要求するクライアント証明書を送らなかった場合、OpenSSLのエラーは「tlsv13 alert certificate required」になります。どちらの値もリクエストごとにリセットされ、TLSのエラーが絡んでいなければ`0`です。
## OpenSSLのエラーを文字列化する
@@ -35,7 +35,7 @@ cli.set_error_logger([](const httplib::Error &err, const httplib::Request *req)
});
```
第2引数の`req`はヌルポインタのこともあります。リクエストを組み立てる前の段階で失敗した場合です。使う前に必ずヌルチェックしてください。
第2引数の`req`はヌルポインタのこともあります。TLSハンドシェイクやCA証明書の読み込み、プロキシのトンネル確立など、接続の準備中にエラーが起きた場合です。使う前に必ずヌルチェックしてください。
## 両方を組み合わせる
+5 -1
View File
@@ -55,7 +55,9 @@ sse.on_error([](httplib::Error err) {
});
```
接続確立時やエラー発生時にもフックを挟めます。エラーハンドラが呼ばれても、`SSEClient`は内部で再接続を試みます。ただし、サーバーが204、403、404のいずれかを返したときは再接続しません。
接続確立時やエラー発生時にもフックを挟めます。エラーハンドラが呼ばれても、`SSEClient`は内部で再接続を試みます。ただし、サーバーが204、403、404のいずれかを返したときや、`set_max_reconnect_attempts()`で決めた回数を使い切ったときは再接続しません。
また、1つのイベントや、まだ改行が来ていない行が`CPPHTTPLIB_SSE_MAX_EVENT_LENGTH`(デフォルトは16MB、0なら無制限)を超えたときも止まります。このときエラーハンドラには`Error::ExceedMaxPayloadSize`が渡されます。イベントストリームには終わりがないので、クライアントの`set_payload_max_length()`はストリーム全体には適用されません。
## 非同期で動かす
@@ -88,6 +90,8 @@ sse.set_max_reconnect_attempts(10); // 10回まで(0=無制限)
`SSEClient`は受信したイベントの`id`を内部で保持していて、再接続時に`Last-Event-ID`ヘッダーとして送ってくれます。この挙動はサーバー側で`id:`付きイベントを送っていれば自動で有効になります。
空の`id:`を受け取ると、保持しているIDはクリアされます。ヘッダーの値として送れない`id:`(NULなどの制御文字を含むものや、前後に空白があるもの)は無視され、それまでのIDがそのまま残ります。
```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
パターンに括弧を使うと、マッチした部分が`req.matches`に入ります。`req.matches[0]`はパス全体、`req.matches[1]`以降がキャプチャです。
正規表現のルートは、`CPPHTTPLIB_REGEX_ROUTE_PATH_MAX_LENGTH`(デフォルトは256)より長いパスには適用されず、マッチしなかったものとして扱われます。長い入力で`std::regex`がスタックを使い果たさないための制限です。もっと長いパスを扱いたい場合は、このマクロに大きな値を定義してください。
## どちらを使うか
- 単純なIDやスラッグなら`:name`でじゅうぶん。読みやすく、型が自明です
@@ -10,12 +10,19 @@ HTML、CSS、画像などの静的ファイルを配信したいときは、`set
```cpp
httplib::Server svr;
svr.set_mount_point("/", "./public");
if (!svr.set_mount_point("/", "./public")) {
// ./publicが存在しない
return 1;
}
svr.listen("0.0.0.0", 8080);
```
`set_mount_point()`はディレクトリが存在しないと`false`を返すので、戻り値を確認しておきましょう。
`./public/index.html`には`http://localhost:8080/index.html`で、`./public/css/style.css`には`http://localhost:8080/css/style.css`でアクセスできます。ディレクトリ構造がそのままURLに反映されます。
ディレクトリへのリクエストには、その中の`index.html`を返します。末尾のスラッシュがないリクエスト(`/docs`)は、`/docs/`へリダイレクト(301)されます。マウントしたディレクトリの外を指すシンボリックリンクには403を返します。
## 複数のマウントポイント
マウントポイントは複数登録できます。
@@ -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(); } // 前のファイルを閉じる
if (file.filename.empty()) { return true; } // 普通のフィールドは読み飛ばす
auto name = httplib::sanitize_filename(file.filename);
if (name.empty()) { return false; } // 安全でない名前は拒否
ofs.open("uploads/" + name, std::ios::binary);
return ofs.is_open();
},
[&](const char *data, size_t len) {
if (!ofs.is_open()) { return true; } // ファイルのパートではない
ofs.write(data, len);
return static_cast<bool>(ofs);
});
if (!ok) { return; } // ステータスは設定済み
res.set_content("uploaded", "text/plain");
});
```
ヘッダーのコールバックはパートごとに呼ばれるので、次のファイルを開く前に前のファイルを閉じています。`file.filename`はクライアントが送ってきた値そのままなので、必ず`httplib::sanitize_filename()`を通してください。空文字列が返ったら、安全でない名前です。
メモリには常に小さなチャンクしか載らないので、ギガバイト級のファイルでも扱えます。
## パート数は自分で数える
@@ -16,7 +16,7 @@ svr.set_pre_routing_handler(
});
```
Pre-routingハンドラは、**ルーティングよりも前**に呼ばれます。どのハンドラにもマッチしないリクエストも含めて、すべてのリクエストを捕まえられます。
Pre-routingハンドラは、**ルーティングよりも前**に呼ばれます。どのハンドラにもマッチしないリクエストも含めて、すべてのリクエストを捕まえられます。ただし、リクエストラインやヘッダーの解析中に拒否されたリクエスト(不正なリクエストの400、長すぎるURIの414、不正な`Range`の416)には、このハンドラが呼ばれる前にサーバーが応答します。
戻り値の`HandlerResponse`がポイントです。
@@ -38,6 +38,8 @@ svr.Options("/.*", [](const auto &req, auto &res) {
});
```
`/.*`は正規表現のルートなので、`CPPHTTPLIB_REGEX_ROUTE_PATH_MAX_LENGTH`(デフォルトは256)より長いパスには適用されません。パスがそれより長くなりうるなら、pre-routingハンドラで`OPTIONS`を処理してください。
## セキュリティヘッダーをまとめて付ける
ブラウザ向けのセキュリティヘッダーを一箇所で管理できます。
@@ -17,6 +17,8 @@ Pre-requestハンドラなら、`req.matched_route`に「マッチしたパタ
Pre-requestハンドラが呼ばれる時点ではボディがまだ読まれていないので、認証に失敗したリクエストなどを、(巨大かもしれない)ボディを読み込む前に拒否できます。その代わり、`req.body`やボディから解析されるフォームフィールドはこの時点では参照できません。ヘッダ・パス・クエリパラメータ・`req.matched_route`を使って判断してください。
Pre-requestハンドラが呼ばれるのは、ルートにマッチしたリクエストだけです。`set_mount_point()`で配信するファイルはルートのマッチングより前に返されるので、静的ファイルを守りたいときはPre-routingハンドラでチェックしてください。
## ルートごとに認証を切り替える
```cpp
@@ -39,6 +41,8 @@ svr.set_pre_request_handler(
`svr.WebSocket()`で登録したルートでも、Pre-requestハンドラは呼ばれます。呼ばれるのは`101 Switching Protocols`を返す前なので、`Handled`を返すとそのHTTPレスポンス(403など)がそのまま返り、WebSocketへのアップグレードは行われません。
サーバーはどの`Origin`からのハンドシェイクも受け付けます。ブラウザは別のサイトが始めたハンドシェイクにもCookieを付けて送るので、Cookieで接続を認証している場合は、`req.get_header_value("Origin")`が自分のオリジンかどうかもここで確認してください。
## 戻り値の意味
Pre-routingハンドラと同じく、`HandlerResponse`を返します。
@@ -4,7 +4,7 @@ order: 32
status: "draft"
---
404や500のような**ハンドラが返したエラーレスポンス**を加工したいときは、`set_error_handler()`を使います。デフォルトの味気ないエラーページを、独自のHTMLやJSONに差し替えられます。
404や500のような**ハンドラが返したエラーレスポンス**を加工したいときは、`set_error_handler()`を使います。エラーレスポンスのボディはデフォルトでは空ですが、エラーハンドラで独自のHTMLやJSONを入れられます。
## 基本の使い方
@@ -4,7 +4,7 @@ order: 34
status: "draft"
---
サーバーが受け取ったリクエストと返したレスポンスをログに残したいときは、`Server::set_logger()`を使います。各リクエストの処理が完了するたびに呼ばれるので、アクセスログやメトリクス収集の土台になります。
サーバーが受け取ったリクエストと返したレスポンスをログに残したいときは、`Server::set_logger()`を使います。リクエストごとに、レスポンスを書き出す直前に呼ばれるので、アクセスログやメトリクス収集の土台になります。
## 基本の使い方
@@ -18,6 +18,8 @@ svr.set_logger([](const httplib::Request &req, const httplib::Response &res) {
ログコールバックには`Request`と`Response`が渡ります。メソッド、パス、ステータスコード、クライアントIP、ヘッダー、ボディなど、好きな情報を取り出せます。
`req.path`はパーセントデコード済みなので、CR/LFのような制御文字を含むことがあります。ログファイルに書き出す前にエスケープしてください。そうしないと、細工したリクエストでログの行を偽造されてしまいます。
## アクセスログ風のフォーマット
Apache / Nginxのアクセスログに似た形式で残す例です。
@@ -38,6 +40,8 @@ svr.set_logger([](const auto &req, const auto &res) {
});
```
マウントポイントから配信するファイルのように、コンテンツプロバイダで送るレスポンスでは、この時点の`res.body`は空なので、サイズは`0B`になります。
## 処理時間を測る
ログで処理時間を出したいときは、pre-routingハンドラで開始時刻を`res.user_data`に保存しておき、ロガーで差分を取ります。
@@ -59,6 +63,8 @@ svr.set_logger([](const httplib::Request &req, const httplib::Response &res) {
});
```
ロガーはレスポンスを書き出す前に呼ばれるので、この時間にボディの送信時間は含まれません。
`user_data`の使い方は[S12. `res.user_data`でハンドラ間データを渡す](../s12-user-data)も参照してください。
> **Note:** ロガーはリクエスト処理と同じスレッドで同期的に呼ばれます。重い処理を直接入れると全体のスループットが落ちるので、必要ならキューに流して非同期で処理しましょう。
> **Note:** ロガーはリクエスト処理と同じスレッドで同期的に呼ばれ、サーバー内のすべてのロガー呼び出しが1つのロックを共有します。重い処理を直接入れると全体のスループットが落ちるので、必要ならキューに流して非同期で処理しましょう。
@@ -55,3 +55,5 @@ int main() {
`stop()`を呼ぶと、新しい接続は受け付けなくなりますが、すでに動いているハンドラは**最後まで実行**されます。ただし、ストリーミングのようにコンテンツプロバイダで送っている途中のレスポンスは、その場で打ち切られます。その後、スレッドプールのワーカーが順次終了し、`listen()`から戻ってきます。これがグレースフルシャットダウンと呼ばれる理由です。
> **Warning:** `stop()`を呼んでから`listen()`が戻るまでには、処理中のリクエストが終わるのを待つ時間がかかります。タイムアウトを強制したい場合は、シャットダウン用のタイマーを別途用意するなど、アプリケーション側の工夫が必要です。
> **Note:** `stop()`はWebSocketハンドラを中断しません。`read()`をループで回しているハンドラは相手がつながっている限り動き続け、それが終わるまで`listen()`も戻りません。開いているWebSocket接続を記録しておき、シャットダウン時に`close()`(`CloseStatus::GoingAway`など)で閉じてください。
+1 -1
View File
@@ -52,6 +52,6 @@ svr.set_keep_alive_max_count(1000);
## スレッドプールとの関係
Keep-Aliveでつながりっぱなしの接続は、その間ずっとワーカースレッドを1つ占有します。接続数 × 同時リクエスト数がスレッドプールのサイズを超えると、新しいリクエストが待たされます。スレッド数の調整は[S21. マルチスレッド数を設定する](../s21-thread-pool)を参照してください。
Keep-Aliveでつながりっぱなしの接続は、その間ずっとワーカースレッドを1つ占有します。スレッドプールは負荷に応じて最大スレッド数(デフォルトはベースの4倍)までスレッドを増やしますが、開いている接続数がそれを超えると、新しい接続はどれかが閉じるまでキューで待たされます。スレッド数の調整は[S21. マルチスレッド数を設定する](../s21-thread-pool)を参照してください。
> **Note:** クライアント側の挙動は[C14. 接続の再利用とKeep-Aliveの挙動を理解する](../c14-keep-alive)を参照してください。サーバーがタイムアウトで接続を切っても、クライアントは自動で再接続します。
@@ -66,4 +66,4 @@ svr.new_task_queue = [] { return new MyTaskQueue(12); };
#include <httplib.h>
```
> **Note:** WebSocket接続はその生存期間中ずっと1スレッドを占有します。大量の同時WebSocket接続を扱うなら、動的スケーリングを有効にしておきましょう(`ThreadPool(8, 64)`のように)。
> **Note:** WebSocket接続はその生存期間中ずっと1スレッドを占有します。大量の同時WebSocket接続を扱うなら、最大スレッド数を引き上げておきましょう(`ThreadPool(8, 64)`のように)。
@@ -46,4 +46,4 @@ wolfSSLには商用ライセンスとサポートがあります。製品に組
- [T03. SSL/TLSサーバーを立ち上げる](../t03-ssl-server)
- [T05. サーバー側でピア証明書を参照する](../t05-peer-cert)
> **Note:** macOSでは、OpenSSL系のバックエンドを使う場合、システムのキーチェーンからルート証明書を自動で読む設定(`CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN`)がデフォルトで有効です。無効にしたい場合は`CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES`を定義してください。
> **Note:** macOSでは、3つのどのバックエンドを使う場合でも、システムのキーチェーンからルート証明書を自動で読む設定(`CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN`)がデフォルトで有効です。無効にしたい場合は`CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES`を定義してください。
@@ -19,6 +19,8 @@ auto res = cli.Get("/");
第1引数がCA証明書ファイル、第2引数がCA証明書ディレクトリ(省略可)です。OpenSSLバックエンドなら、`set_ca_cert_store()`で`X509_STORE*`を直接渡すこともできます。
独自のCAを指定すると、クライアントはそのCAだけを信頼し、OSの証明書ストアは読みません。両方を信頼したい場合は、`cli.enable_system_ca(true)`も呼んでください。CA証明書はクライアントの最初の接続で一度だけ読み込まれるので、これらの設定は最初のリクエストより前に済ませておきましょう。
## 証明書検証を無効にする(非推奨)
開発用のサーバーや自己署名証明書にアクセスしたいときは、検証を無効にできます。
@@ -46,7 +48,7 @@ cli.enable_server_hostname_verification(false);
## OSの証明書ストアをそのまま使う
多くのLinuxディストリビューションでは、`/etc/ssl/certs/ca-certificates.crt`などにルート証明書がまとまっています。cpp-httplibは起動時にOSのデフォルトストアを自動で読みにいくので、普通のサーバーならとくに設定不要です。
多くのLinuxディストリビューションでは、`/etc/ssl/certs/ca-certificates.crt`などにルート証明書がまとまっています。cpp-httplibはクライアントの最初の接続でOSのデフォルトストアを自動で読みにいくので、普通のサーバーならとくに設定不要です。
> mbedTLSやwolfSSLバックエンドでも同じAPIが使えます。バックエンドの選び方は[T01. OpenSSL・mbedTLS・wolfSSLの選択指針](../t01-tls-backends)を参照してください。
+2 -2
View File
@@ -44,7 +44,7 @@ for (const auto &san : sans) {
}
```
ホスト名がSANに含まれるかを確認するヘルパーもあります。
証明書がホスト名をカバーしているかを確認するヘルパーもあります。TLSのホスト名検証と同じ規則で、IPアドレスはIPアドレスのSANと、ホスト名はDNS名のSANと照合し、CNはDNS名のSANがないときだけ使います。
```cpp
if (cert.check_hostname("alice.corp.example.com")) {
@@ -81,7 +81,7 @@ Pre-requestハンドラと組み合わせれば、共通の認可ロジックを
## SNI(Server Name Indication)
クライアントが指定してきたサーバー名は、cpp-httplibが自動で処理します。同じサーバーで複数のドメインをホストする場合にSNIが使われますが、通常はハンドラ側で意識する必要はありません。
SNIはクライアントが自動で送ります。サーバー側では、`req.sni()`でクライアントが指定したサーバー名を取得できます。ただし`SSLServer`はその名前に関係なく同じ証明書を提示するので、1つのサーバーで複数のドメインをホストするなら、すべてのドメインをカバーする証明書を使ってください。名前ごとに証明書を切り替えたい場合は、TLSバックエンドの機能を直接使います。たとえばOpenSSLなら、コンテキストのセットアップ用コールバックを受け取る`SSLServer`のコンストラクタで、servernameコールバックを設定します。
> **Warning:** `req.peer_cert()`は、mTLSが有効で、かつクライアントが証明書を提示した場合のみ有効な値を返します。通常のTLS接続では空の`PeerCert`が返ります。使う前に必ず`bool`チェックしてください。
@@ -21,7 +21,12 @@ int main() {
if (result == httplib::ws::ReadResult::Fail) {
break;
}
ws.send(msg); // 受け取った内容をそのまま返す
// 受け取った内容を同じフレームタイプでそのまま返す
if (result == httplib::ws::ReadResult::Binary) {
ws.send(msg.data(), msg.size());
} else {
ws.send(msg);
}
}
});
@@ -29,7 +34,7 @@ int main() {
}
```
`svr.WebSocket()`でWebSocket用のハンドラを登録します。ハンドラが呼ばれた時点で、すでにWebSocketのハンドシェイクは完了しています。ループの中で`ws.read()`して`ws.send()`するだけで、エコー動作が完成します。
`svr.WebSocket()`でWebSocket用のハンドラを登録します。ハンドラが呼ばれた時点で、すでにWebSocketのハンドシェイクは完了しています。ループの中で`ws.read()`して`ws.send()`するだけで、エコー動作が完成します。バイナリメッセージは`data()`と`size()`を渡すオーバーロードで返すので、バイナリのまま届きます(詳しくは後述)。
`read()`の返り値は`ReadResult`列挙値で、次の4種類です。
@@ -76,7 +81,7 @@ ws.send(binary_data, binary_data_size); // バイナリフレーム
## スレッドとの関係
WebSocket接続はハンドラが終わるまで生き続けるので、1接続につきワーカースレッドを1つ占有します。同時接続数が多い場合は、スレッドプールを動的スケーリングに設定しましょう。
WebSocket接続はハンドラが終わるまで生き続けるので、1接続につきワーカースレッドを1つ占有します。デフォルトのスレッドプールも負荷に応じてベースの4倍まで増えますが、同時接続数が多い場合は、その最大スレッド数を引き上げましょう。
```cpp
svr.new_task_queue = [] {
@@ -43,7 +43,7 @@ cli.connect();
## Pingへの応答(Pong)
WebSocketプロトコルでは、PingフレームにはPongフレームで応答することが決まっています。cpp-httplibは受信したPingに自動でPongを返すので、アプリケーションコード側で気にする必要はありません。
WebSocketプロトコルでは、PingフレームにはPongフレームで応答することが決まっています。cpp-httplibは受信したPingに`read()`の中で自動でPongを返すので、`read()`を呼び続けている限り、アプリケーションコード側で気にする必要はありません。
## Pingの間隔をどう決めるか
@@ -67,7 +67,7 @@ cli.close(httplib::ws::CloseStatus::Normal);
ws.close(httplib::ws::CloseStatus::GoingAway, "server restarting");
```
クライアント側はこのステータスを見て、再接続を試みるかどうかを判断できます。
クライアント側の`read()`は`Fail`を返すだけで、受け取ったステータスや理由は取り出せません。再起動による切断をほかの切断と見分けたい場合は、先にメッセージ(`ws.send("restarting")`など)を送ってから`close()`を呼びましょう。
## サンプル: 簡単なチャット終了
@@ -85,4 +85,4 @@ if (cli.read(buf) == httplib::ws::ReadResult::Binary) {
テキストとバイナリを混ぜて送ることもできます。たとえば「制御メッセージはJSON、データ本体はバイナリ」といったプロトコルを組み立てると、メタデータと生データを効率よく扱えます。
> **Note:** WebSocketのフレームサイズには上限がないわけではありません。巨大なデータを送るときは、アプリケーション側で分割して送るのが安全です。cpp-httplibのデフォルトでは大きなフレームもそのまま処理されますが、メモリを一気に使う点は変わりません。
> **Note:** 受信できるメッセージの大きさは`CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH`(デフォルトは16MB、0なら無制限)までです。これを超えると`read()`は`Fail`を返し、接続は終了します。上限以内でもメッセージはまるごとメモリに載るので、巨大なデータを送るときは、アプリケーション側で分割して送るのが安全です。
@@ -26,6 +26,8 @@ if (ws.connect()) {
証明書検証そのものを無効にしたい場合は`enable_server_certificate_verification(false)`が使えます。挙動の詳細は[T02. SSL証明書の検証を制御する](../t02-cert-verification)を参照してください。
`WebSocketClient`には、独自の検証に使う`set_server_certificate_verifier()`と`set_session_verifier()`や、プロキシ用のセッター(`set_proxy()`、`set_proxy_basic_auth()`、`set_proxy_bearer_token_auth()`、`set_proxy_digest_auth()`、`set_no_proxy()`)もあります。CA証明書は最初の`connect()`で読み込まれるので、それより前に設定してください。
## クライアント証明書を使う(mTLS)
`ws::WebSocketClient`には`PemMemory`構造体を受け取るコンストラクタがあり、`wss://`接続でクライアント証明書を提示できます。
+1 -1
View File
@@ -9,7 +9,7 @@ C++でちょっとしたHTTPサーバーやクライアントが必要になっ
APIはラムダ式をベースにした直感的な設計で、C++11以降のコンパイラーがあればどこでも動きます。Windows、macOS、Linux — お使いの環境をそのまま使えます。
HTTPSも使えます。OpenSSLやmbedTLSをリンクするだけで、サーバー・クライアントの両方がTLSに対応します。Content-Encoding(gzip, brotli等)、ファイルアップロードなど、実際の開発で必要になる機能もひと通り揃っています。WebSocketもサポートしています。
HTTPSも使えます。OpenSSL、Mbed TLS、wolfSSLのいずれかをリンクするだけで、サーバー・クライアントの両方がTLSに対応します。Content-Encoding(gzip, brotli等)、ファイルアップロードなど、実際の開発で必要になる機能もひと通り揃っています。WebSocketもサポートしています。
内部的にはブロッキングI/Oとスレッドプールを使っています。大量の同時接続を捌くような用途には向きませんが、APIサーバーやツールの組み込みHTTP、テスト用のモックサーバーなど、多くのユースケースで十分な性能を発揮します。
@@ -76,7 +76,7 @@ endif()
## 4.2 モデル一覧を定義する
アプリが扱えるモデルの一覧を定義します。翻訳タスクで検証済みの4モデルを用意しました。
アプリが扱えるモデルの一覧を定義します。翻訳タスクで検証済みの3モデルを用意しました。
```cpp
struct ModelInfo {
+10 -2
View File
@@ -875,6 +875,7 @@ modelSelect.addEventListener("change", async () => {
#include <filesystem>
#include <fstream>
#include <iostream>
#include <mutex>
using json = nlohmann::json;
@@ -1006,6 +1007,7 @@ int main() {
std::cout << std::endl;
}
auto llm = llamalib::Llama{path};
std::mutex llm_mutex; // モデル切り替え中のアクセスを保護する
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パース・バリデーション(詳細は2章を参照)
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");
@@ -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: "
@@ -1168,8 +1173,11 @@ int main() {
// モデルをロードして切り替え
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();
@@ -675,9 +675,10 @@ int main() {
5章からの変更点をまとめると:
- `#include <csignal>` → `#include <thread>`, `<cpp-embedlib-httplib.h>`, `"WebAssets.h"`, `"webview/webview.h"`
- `signal_handler`関数を削除
- `signal_handler`関数を削除し、`svr`を`main()`の中に移動
- `svr.set_mount_point("/", "./public")` → `httplib::mount(svr, Web::FS)`
- `svr.listen("127.0.0.1", 8080)` → `bind_to_any_port` + `listen_after_bind` + WebViewのイベントループ
- macOS用に`setup_macos_edit_menu()`と`<objc/objc-runtime.h>`のインクルードを追加
ハンドラのコードは1行も変わっていません。5章まで作ってきたREST API・SSEストリーミング・モデル管理がそのまま動きます。
@@ -59,7 +59,7 @@ response = client.chat.completions.create(
## 7.3 同時リクエスト処理
私たちのサーバーはリクエストを1つずつ処理します。翻訳中に別のリクエストが来ると、前の推論が終わるまで待ちます。1人で使うデスクトップアプリなら問題ありませんが、複数人で共有するサーバーでは困ります。
私たちのサーバーは推論を1リクエストずつ実行します。翻訳中に別のリクエストが来ると、前の推論が終わるまで待ちます。1人で使うデスクトップアプリなら問題ありませんが、複数人で共有するサーバーでは困ります。
`llama-server`は**スロット**という仕組みで同時リクエストを処理します。
@@ -67,7 +67,7 @@ response = client.chat.completions.create(
ポイントは、各スロットのトークンを**1つずつ順番に**ではなく、**まとめて1回のバッチ**で推論することです。GPUは並列処理が得意なので、2人分を同時に処理しても1人分とほとんど変わらない時間で済みます。これを「連続バッチ処理(continuous batching)」と呼びます。
私たちのサーバーではcpp-httplibのスレッドプールが各リクエストに1スレッドを割り当てますが、推論自体は`llm.chat()`の中でシングルスレッドです。`llama-server`はこの推論部分を共有のバッチ処理ループに集約しています。
私たちのサーバーではcpp-httplibのスレッドプールが接続ごとにワーカースレッドを1つ割り当てるので、別々の接続から来たリクエストは並行して処理されます。ただし推論は`llm_mutex`で直列化されるので、`llm.chat()`は一度に1つしか実行されません。`llama-server`はこの推論部分を共有のバッチ処理ループに集約しています。
## 7.4 SSEフォーマットの違い
+3 -1
View File
@@ -21,7 +21,9 @@ if (result) {
}
```
`Get()`に`content_receiver`コールバックを渡す方法もあります。こちらはKeep-Aliveと併用できます。
なお、`stream::Get()`で読めるボディの大きさは、クライアントの`set_payload_max_length()`の上限(デフォルトは100MB)までです。もっと大きなファイルをダウンロードするなら、上限を引き上げるか、0を指定して無制限にしてください。
`Get()`に`content_receiver`コールバックを渡す方法もあります。こちらはKeep-Aliveと併用でき、`set_payload_max_length()`で上限を設定しない限りサイズの制限もありません。
```cpp
httplib::Client cli("http://localhost:8080");