Add Cookbook S01-S22 (draft)

This commit is contained in:
yhirose
2026-04-10 18:47:42 -04:00
parent 61e533ddc5
commit 361b753f19
45 changed files with 2562 additions and 44 deletions

View File

@@ -42,36 +42,36 @@ A collection of recipes that answer "How do I...?" questions. Each recipe is sel
## Server
### Basics
- S01. Register GET / POST / PUT / DELETE handlers
- S02. Receive JSON requests and return JSON responses
- S03. Use path parameters (`/users/:id`)
- S04. Set up a static file server (`set_mount_point`)
- [S01. Register GET / POST / PUT / DELETE handlers](s01-handlers)
- [S02. Receive JSON requests and return JSON responses](s02-json-api)
- [S03. Use path parameters](s03-path-params)
- [S04. Set up a static file server](s04-static-files)
### Streaming & Files
- S05. Stream a large file in the response (`ContentProvider`)
- S06. Return a file download response (`Content-Disposition`)
- S07. Receive multipart data as a stream (`ContentReader`)
- S08. Compress responses (gzip)
- [S05. Stream a large file in the response](s05-stream-response)
- [S06. Return a file download response](s06-download-response)
- [S07. Receive multipart data as a stream](s07-multipart-reader)
- [S08. Return a compressed response](s08-compress-response)
### Handler Chain
- S09. Add pre-processing to all routes (Pre-routing handler)
- S10. Add response headers with a Post-routing handler (CORS, etc.)
- S11. Authenticate per route with a Pre-request handler (`matched_route`)
- S12. Pass data between handlers with `res.user_data`
- [S09. Add pre-processing to all routes](s09-pre-routing)
- [S10. Add response headers with a post-routing handler](s10-post-routing)
- [S11. Authenticate per route with a pre-request handler](s11-pre-request)
- [S12. Pass data between handlers with `res.user_data`](s12-user-data)
### Error Handling & Debugging
- S13. Return custom error pages (`set_error_handler`)
- S14. Catch exceptions (`set_exception_handler`)
- S15. Log requests (Logger)
- S16. Detect client disconnection (`req.is_connection_closed()`)
- [S13. Return custom error pages](s13-error-handler)
- [S14. Catch exceptions](s14-exception-handler)
- [S15. Log requests](s15-server-logger)
- [S16. Detect client disconnection](s16-disconnect)
### Operations & Tuning
- S17. Assign a port dynamically (`bind_to_any_port`)
- S18. Control startup order with `listen_after_bind`
- S19. Shut down gracefully (`stop()` and signal handling)
- S20. Tune Keep-Alive (`set_keep_alive_max_count` / `set_keep_alive_timeout`)
- S21. Configure the thread pool (`new_task_queue`)
- S22. Communicate over Unix domain sockets (`set_address_family(AF_UNIX)`)
- [S17. Bind to any available port](s17-bind-any-port)
- [S18. Control startup order with `listen_after_bind`](s18-listen-after-bind)
- [S19. Shut down gracefully](s19-graceful-shutdown)
- [S20. Tune Keep-Alive](s20-keep-alive)
- [S21. Configure the thread pool](s21-thread-pool)
- [S22. Talk over a Unix domain socket](s22-unix-socket)
## TLS / Security

View File

@@ -0,0 +1,66 @@
---
title: "S01. Register GET / POST / PUT / DELETE Handlers"
order: 20
status: "draft"
---
With `httplib::Server`, you register a handler per HTTP method. Just pass a pattern and a lambda to `Get()`, `Post()`, `Put()`, or `Delete()`.
## Basic usage
```cpp
#include <httplib.h>
int main() {
httplib::Server svr;
svr.Get("/hello", [](const httplib::Request &req, httplib::Response &res) {
res.set_content("Hello, World!", "text/plain");
});
svr.Post("/api/items", [](const httplib::Request &req, httplib::Response &res) {
// req.body holds the request body
res.status = 201;
res.set_content("Created", "text/plain");
});
svr.Put("/api/items/1", [](const httplib::Request &req, httplib::Response &res) {
res.set_content("Updated", "text/plain");
});
svr.Delete("/api/items/1", [](const httplib::Request &req, httplib::Response &res) {
res.status = 204;
});
svr.listen("0.0.0.0", 8080);
}
```
Handlers take `(const Request&, Response&)`. Use `res.set_content()` to set the body and Content-Type, and `res.status` for the status code. `listen()` starts the server and blocks.
## Read query parameters
```cpp
svr.Get("/search", [](const httplib::Request &req, httplib::Response &res) {
auto q = req.get_param_value("q");
auto limit = req.get_param_value("limit");
res.set_content("q=" + q + ", limit=" + limit, "text/plain");
});
```
`req.get_param_value()` pulls a value from the query string. Use `req.has_param("q")` if you want to check existence first.
## Read request headers
```cpp
svr.Get("/me", [](const httplib::Request &req, httplib::Response &res) {
auto ua = req.get_header_value("User-Agent");
res.set_content("UA: " + ua, "text/plain");
});
```
To add a response header, use `res.set_header("Name", "Value")`.
> **Note:** `listen()` is a blocking call. To run it on a different thread, wrap it in `std::thread`. If you need non-blocking startup, see S18. Control Startup Order with `listen_after_bind`.
> To use path parameters like `/users/:id`, see S03. Use Path Parameters.

View File

@@ -0,0 +1,74 @@
---
title: "S02. Receive a JSON Request and Return a JSON Response"
order: 21
status: "draft"
---
cpp-httplib doesn't include a JSON parser. On the server side, combine it with something like [nlohmann/json](https://github.com/nlohmann/json). The examples below use `nlohmann/json`.
## Receive and return JSON
```cpp
#include <httplib.h>
#include <nlohmann/json.hpp>
int main() {
httplib::Server svr;
svr.Post("/api/users", [](const httplib::Request &req, httplib::Response &res) {
try {
auto in = nlohmann::json::parse(req.body);
nlohmann::json out = {
{"id", 42},
{"name", in["name"]},
{"created_at", "2026-04-10T12:00:00Z"},
};
res.status = 201;
res.set_content(out.dump(), "application/json");
} catch (const std::exception &e) {
res.status = 400;
res.set_content("{\"error\":\"invalid json\"}", "application/json");
}
});
svr.listen("0.0.0.0", 8080);
}
```
`req.body` is a plain `std::string`, so you pass it straight to your JSON library. For the response, `dump()` to a string and set the Content-Type to `application/json`.
## Check the Content-Type
```cpp
svr.Post("/api/users", [](const httplib::Request &req, httplib::Response &res) {
auto content_type = req.get_header_value("Content-Type");
if (content_type.find("application/json") == std::string::npos) {
res.status = 415; // Unsupported Media Type
return;
}
// ...
});
```
When you strictly want JSON only, verify the Content-Type up front.
## A helper for JSON responses
If you're writing the same pattern repeatedly, a small helper saves typing.
```cpp
auto send_json = [](httplib::Response &res, int status, const nlohmann::json &j) {
res.status = status;
res.set_content(j.dump(), "application/json");
};
svr.Get("/api/health", [&](const auto &req, auto &res) {
send_json(res, 200, {{"status", "ok"}});
});
```
> **Note:** A large JSON body ends up entirely in `req.body`, which means it all sits in memory. For huge payloads, consider streaming reception — see S07. Receive Multipart Data as a Stream.
> For the client side, see C02. Send and Receive JSON.

View File

@@ -0,0 +1,53 @@
---
title: "S03. Use Path Parameters"
order: 22
status: "draft"
---
For dynamic URLs like `/users/:id` — the staple of REST APIs — just put `:name` in the path pattern. The matched values end up in `req.path_params`.
## Basic usage
```cpp
svr.Get("/users/:id", [](const httplib::Request &req, httplib::Response &res) {
auto id = req.path_params.at("id");
res.set_content("user id: " + id, "text/plain");
});
```
A request to `/users/42` fills `req.path_params["id"]` with `"42"`. `path_params` is a `std::unordered_map<std::string, std::string>`, so use `at()` to read it.
## Multiple parameters
You can have as many as you need.
```cpp
svr.Get("/orgs/:org/repos/:repo", [](const httplib::Request &req, httplib::Response &res) {
auto org = req.path_params.at("org");
auto repo = req.path_params.at("repo");
res.set_content(org + "/" + repo, "text/plain");
});
```
This matches paths like `/orgs/anthropic/repos/cpp-httplib`.
## Regex patterns
For more flexible matching, use a `std::regex`-based pattern.
```cpp
svr.Get(R"(/users/(\d+))", [](const httplib::Request &req, httplib::Response &res) {
auto id = req.matches[1];
res.set_content("user id: " + std::string(id), "text/plain");
});
```
Parentheses in the pattern become captures in `req.matches`. `req.matches[0]` is the full match; `req.matches[1]` onward are the captures.
## Which to use
- For plain IDs or slugs, `:name` is enough — readable, and the shape is obvious
- Use regex when you want to constrain the URL to, say, numbers only or a UUID format
- Mixing both can get confusing — stick with one style per project
> **Note:** Path parameters come in as strings. If you need an integer, convert with `std::stoi()` and don't forget to handle conversion errors.

View File

@@ -0,0 +1,55 @@
---
title: "S04. Serve Static Files"
order: 23
status: "draft"
---
To serve static files like HTML, CSS, and images, use `set_mount_point()`. Just map a URL path to a local directory, and the whole directory becomes accessible.
## Basic usage
```cpp
httplib::Server svr;
svr.set_mount_point("/", "./public");
svr.listen("0.0.0.0", 8080);
```
`./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.
## Multiple mount points
You can register more than one mount point.
```cpp
svr.set_mount_point("/", "./public");
svr.set_mount_point("/assets", "./dist/assets");
svr.set_mount_point("/uploads", "./var/uploads");
```
You can even mount multiple directories at the same path — they're searched in registration order, and the first hit wins.
## Combine with API handlers
Static files and API handlers coexist happily. Handlers registered with `Get()` and friends take priority; the mount points are searched only when nothing matches.
```cpp
svr.Get("/api/users", [](const auto &req, auto &res) {
res.set_content("[]", "application/json");
});
svr.set_mount_point("/", "./public");
```
This gives you an SPA-friendly setup: `/api/*` hits the handlers, everything else is served from `./public/`.
## Add MIME types
cpp-httplib ships with a built-in extension-to-Content-Type map, but you can add your own.
```cpp
svr.set_file_extension_and_mimetype_mapping("wasm", "application/wasm");
```
> **Warning:** The static file server methods are **not thread-safe**. Don't call them after `listen()` — configure everything before starting the server.
> For download-style responses, see S06. Return a File Download Response.

View File

@@ -0,0 +1,63 @@
---
title: "S05. Stream a Large File in the Response"
order: 24
status: "draft"
---
When the response is a huge file or data generated on the fly, loading the whole thing into memory isn't realistic. Use `Response::set_content_provider()` to produce data in chunks as you send it.
## When the size is known
```cpp
svr.Get("/download", [](const httplib::Request &req, httplib::Response &res) {
size_t total_size = get_file_size("large.bin");
res.set_content_provider(
total_size, "application/octet-stream",
[](size_t offset, size_t length, httplib::DataSink &sink) {
auto data = read_range_from_file("large.bin", offset, length);
sink.write(data.data(), data.size());
return true;
});
});
```
The lambda is called repeatedly with `offset` and `length`. Read just that range and write it to `sink`. Only a small chunk sits in memory at any given time.
## Just send a file
If you only want to serve a file, `set_file_content()` is far simpler.
```cpp
svr.Get("/download", [](const httplib::Request &req, httplib::Response &res) {
res.set_file_content("large.bin", "application/octet-stream");
});
```
It streams internally, so even huge files are safe. Omit the Content-Type and it's guessed from the extension.
## When the size is unknown — chunked transfer
For data generated on the fly, where you don't know the total size up front, use `set_chunked_content_provider()`. It's sent with HTTP chunked transfer encoding.
```cpp
svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
res.set_chunked_content_provider(
"text/plain",
[](size_t offset, httplib::DataSink &sink) {
auto chunk = produce_next_chunk();
if (chunk.empty()) {
sink.done(); // done sending
return true;
}
sink.write(chunk.data(), chunk.size());
return true;
});
});
```
Call `sink.done()` to signal the end.
> **Note:** The provider lambda is called multiple times. Watch out for the lifetime of captured variables — wrap them in a `std::shared_ptr` if needed.
> To serve the file as a download, see S06. Return a File Download Response.

View File

@@ -0,0 +1,50 @@
---
title: "S06. Return a File Download Response"
order: 25
status: "draft"
---
To force a browser to show a **download dialog** instead of rendering inline, send a `Content-Disposition` header. There's no special cpp-httplib API for this — it's just a header.
## Basic usage
```cpp
svr.Get("/download/report", [](const httplib::Request &req, httplib::Response &res) {
res.set_header("Content-Disposition", "attachment; filename=\"report.pdf\"");
res.set_file_content("reports/2026-04.pdf", "application/pdf");
});
```
`Content-Disposition: attachment` makes the browser pop up a "Save As" dialog. The `filename=` parameter becomes the default save name.
## Non-ASCII file names
For file names with non-ASCII characters or spaces, use the RFC 5987 `filename*` form.
```cpp
svr.Get("/download/report", [](const httplib::Request &req, httplib::Response &res) {
res.set_header(
"Content-Disposition",
"attachment; filename=\"report.pdf\"; "
"filename*=UTF-8''%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88.pdf");
res.set_file_content("reports/2026-04.pdf", "application/pdf");
});
```
The part after `filename*=UTF-8''` is URL-encoded UTF-8. Keep the ASCII `filename=` too, as a fallback for older browsers.
## Download dynamically generated data
You don't need a real file — you can serve a generated string as a download directly.
```cpp
svr.Get("/export.csv", [](const httplib::Request &req, httplib::Response &res) {
std::string csv = build_csv();
res.set_header("Content-Disposition", "attachment; filename=\"export.csv\"");
res.set_content(csv, "text/csv");
});
```
This is the classic pattern for CSV exports.
> **Note:** Some browsers will trigger a download based on Content-Type alone, even without `Content-Disposition`. Conversely, setting `inline` tries to render the content in the browser when possible.

View File

@@ -0,0 +1,71 @@
---
title: "S07. Receive Multipart Data as a Stream"
order: 26
status: "draft"
---
A naive upload handler puts the whole request into `req.body`, which blows up memory for large files. Use `HandlerWithContentReader` to receive the body chunk by chunk.
## Basic usage
```cpp
svr.Post("/upload",
[](const httplib::Request &req, httplib::Response &res,
const httplib::ContentReader &content_reader) {
if (req.is_multipart_form_data()) {
content_reader(
// headers of each part
[&](const httplib::FormData &file) {
std::cout << "name: " << file.name
<< ", filename: " << file.filename << std::endl;
return true;
},
// body of each part (called multiple times)
[&](const char *data, size_t len) {
// write to disk here, for example
return true;
});
} else {
// plain request body
content_reader([&](const char *data, size_t len) {
return true;
});
}
res.set_content("ok", "text/plain");
});
```
The `content_reader` has two call shapes. For multipart data, pass two callbacks (one for headers, one for body). For plain bodies, pass just one.
## Write directly to disk
Here's how to stream an uploaded file to disk.
```cpp
svr.Post("/upload",
[](const httplib::Request &req, httplib::Response &res,
const httplib::ContentReader &content_reader) {
std::ofstream ofs;
content_reader(
[&](const httplib::FormData &file) {
if (!file.filename.empty()) {
ofs.open("uploads/" + file.filename, std::ios::binary);
}
return static_cast<bool>(ofs);
},
[&](const char *data, size_t len) {
ofs.write(data, len);
return static_cast<bool>(ofs);
});
res.set_content("uploaded", "text/plain");
});
```
Only a small chunk sits in memory at any moment, so gigabyte-scale files are no problem.
> **Warning:** When you use `HandlerWithContentReader`, `req.body` stays **empty**. Handle the body yourself inside the callbacks.
> For the client side of multipart uploads, see C07. Upload a File as Multipart Form Data.

View File

@@ -0,0 +1,53 @@
---
title: "S08. Return a Compressed Response"
order: 27
status: "draft"
---
cpp-httplib automatically compresses response bodies when the client indicates support via `Accept-Encoding`. The handler doesn't need to do anything special. Supported encodings are gzip, Brotli, and Zstd.
## Build-time setup
To enable compression, define the relevant macros before including `httplib.h`:
```cpp
#define CPPHTTPLIB_ZLIB_SUPPORT // gzip
#define CPPHTTPLIB_BROTLI_SUPPORT // brotli
#define CPPHTTPLIB_ZSTD_SUPPORT // zstd
#include <httplib.h>
```
You'll also need to link `zlib`, `brotli`, and `zstd` respectively. Enable only what you need.
## Usage
```cpp
svr.Get("/api/data", [](const httplib::Request &req, httplib::Response &res) {
std::string body = build_large_response();
res.set_content(body, "application/json");
});
```
That's it. If the client sent `Accept-Encoding: gzip`, cpp-httplib compresses the response with gzip automatically. `Content-Encoding: gzip` and `Vary: Accept-Encoding` are added for you.
## Encoding priority
When the client accepts multiple encodings, cpp-httplib picks in this order (among those enabled at build time): Brotli → Zstd → gzip. Your code doesn't need to care — you always get the most efficient option available.
## Streaming responses are compressed too
Streaming responses via `set_chunked_content_provider()` get the same automatic compression.
```cpp
svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
res.set_chunked_content_provider(
"text/plain",
[](size_t offset, httplib::DataSink &sink) {
// ...
});
});
```
> **Note:** Tiny responses barely benefit from compression and just waste CPU time. cpp-httplib skips compression for bodies that are too small to bother with.
> For the client-side counterpart, see C15. Enable Compression.

View File

@@ -0,0 +1,54 @@
---
title: "S09. Add Pre-Processing to All Routes"
order: 28
status: "draft"
---
Sometimes you want the same logic to run before every request — auth checks, logging, rate limiting. Register those with `set_pre_routing_handler()`.
## Basic usage
```cpp
svr.set_pre_routing_handler(
[](const httplib::Request &req, httplib::Response &res) {
std::cout << req.method << " " << req.path << std::endl;
return httplib::Server::HandlerResponse::Unhandled;
});
```
The pre-routing handler runs **before routing**. It catches every request — including ones that don't match any handler.
The `HandlerResponse` return value is key:
- Return `Unhandled` → continue normally (routing and the actual handler run)
- Return `Handled` → the response is considered complete, skip the rest
## Use it for authentication
Put your shared auth check in one place.
```cpp
svr.set_pre_routing_handler(
[](const httplib::Request &req, httplib::Response &res) {
if (req.path.rfind("/public", 0) == 0) {
return httplib::Server::HandlerResponse::Unhandled; // no auth needed
}
auto auth = req.get_header_value("Authorization");
if (auth.empty()) {
res.status = 401;
res.set_content("unauthorized", "text/plain");
return httplib::Server::HandlerResponse::Handled;
}
return httplib::Server::HandlerResponse::Unhandled;
});
```
If auth fails, return `Handled` to respond with 401 immediately. If it passes, return `Unhandled` and let routing take over.
## For per-route auth
If you want different auth rules per route rather than a single global check, `set_pre_request_handler()` is a better fit. See S11. Authenticate Per Route with a Pre-Request Handler.
> **Note:** If all you want is to modify the response, `set_post_routing_handler()` is the right tool. See S10. Add Response Headers with a Post-Routing Handler.

View File

@@ -0,0 +1,56 @@
---
title: "S10. Add Response Headers with a Post-Routing Handler"
order: 29
status: "draft"
---
Sometimes you want to add shared headers to the response after the handler has run — CORS headers, security headers, a request ID, and so on. That's what `set_post_routing_handler()` is for.
## Basic usage
```cpp
svr.set_post_routing_handler(
[](const httplib::Request &req, httplib::Response &res) {
res.set_header("X-Request-ID", generate_request_id());
});
```
The post-routing handler runs **after the route handler, before the response is sent**. From here you can call `res.set_header()` or `res.headers.erase()` to add or remove headers across every response in one place.
## Add CORS headers
CORS is a classic use case.
```cpp
svr.set_post_routing_handler(
[](const httplib::Request &req, httplib::Response &res) {
res.set_header("Access-Control-Allow-Origin", "*");
res.set_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.set_header("Access-Control-Allow-Headers", "Content-Type, Authorization");
});
```
For the preflight `OPTIONS` requests, register a separate handler — or handle them in the pre-routing handler.
```cpp
svr.Options("/.*", [](const auto &req, auto &res) {
res.status = 204;
});
```
## Bundle your security headers
Manage browser security headers in one spot.
```cpp
svr.set_post_routing_handler(
[](const httplib::Request &req, httplib::Response &res) {
res.set_header("X-Content-Type-Options", "nosniff");
res.set_header("X-Frame-Options", "DENY");
res.set_header("Referrer-Policy", "strict-origin-when-cross-origin");
});
```
No matter which handler produced the response, the same headers get attached.
> **Note:** The post-routing handler also runs for responses that didn't match any route and for responses from error handlers. That's exactly what you want when you need certain headers on every response, guaranteed.

View File

@@ -0,0 +1,47 @@
---
title: "S11. Authenticate Per Route with a Pre-Request Handler"
order: 30
status: "draft"
---
The `set_pre_routing_handler()` from S09 runs **before routing**, so it has no idea which route matched. When you want per-route behavior, `set_pre_request_handler()` is what you need.
## Pre-routing vs. pre-request
| Hook | When it runs | Route info |
| --- | --- | --- |
| `set_pre_routing_handler` | Before routing | Not available |
| `set_pre_request_handler` | After routing, right before the route handler | Available via `req.matched_route` |
In a pre-request handler, `req.matched_route` holds the **pattern string** that matched. You can vary behavior based on the route definition itself.
## Switch auth per route
```cpp
svr.set_pre_request_handler(
[](const httplib::Request &req, httplib::Response &res) {
// require auth for routes starting with /admin
if (req.matched_route.rfind("/admin", 0) == 0) {
auto token = req.get_header_value("Authorization");
if (!is_admin_token(token)) {
res.status = 403;
res.set_content("forbidden", "text/plain");
return httplib::Server::HandlerResponse::Handled;
}
}
return httplib::Server::HandlerResponse::Unhandled;
});
```
`matched_route` is the pattern **before** path parameters are expanded (e.g. `/admin/users/:id`). You compare against the route definition, not the actual request path, so IDs or names don't throw you off.
## Return values
Same as pre-routing — return `HandlerResponse`.
- `Unhandled`: continue (the route handler runs)
- `Handled`: we're done, skip the route handler
## Passing auth info to the route handler
To pass decoded user info into the route handler, use `res.user_data`. See S12. Pass Data Between Handlers with `res.user_data`.

View File

@@ -0,0 +1,56 @@
---
title: "S12. Pass Data Between Handlers with res.user_data"
order: 31
status: "draft"
---
Say your pre-request handler decodes an auth token and you want the route handler to use the result. That "data handoff between handlers" is what `res.user_data` is for — it holds values of arbitrary types.
## Basic usage
```cpp
struct AuthUser {
std::string id;
std::string name;
bool is_admin;
};
svr.set_pre_request_handler(
[](const httplib::Request &req, httplib::Response &res) {
auto token = req.get_header_value("Authorization");
auto user = decode_token(token); // decode the auth token
res.user_data.set("user", user);
return httplib::Server::HandlerResponse::Unhandled;
});
svr.Get("/me", [](const httplib::Request &req, httplib::Response &res) {
auto *user = res.user_data.get<AuthUser>("user");
if (!user) {
res.status = 401;
return;
}
res.set_content("Hello, " + user->name, "text/plain");
});
```
`user_data.set()` stores a value of any type, and `user_data.get<T>()` retrieves it. If you give the wrong type you get `nullptr` back — so be careful.
## Typical value types
Strings, numbers, structs, `std::shared_ptr` — anything copyable or movable works.
```cpp
res.user_data.set("user_id", std::string{"42"});
res.user_data.set("is_admin", true);
res.user_data.set("started_at", std::chrono::steady_clock::now());
```
## Where to set, where to read
The usual flow is: set it in `set_pre_routing_handler()` or `set_pre_request_handler()`, read it in the route handler. Pre-request runs after routing, so you can combine it with `req.matched_route` to set values only for specific routes.
## A gotcha
`user_data` lives on `Response`, not `Request`. That's because handlers get `Response&` (mutable) but only `const Request&`. It looks odd at first, but it makes sense once you think of it as "the mutable context shared between handlers."
> **Warning:** `user_data.get<T>()` returns `nullptr` when the type doesn't match. Use the exact same type on set and get. Storing as `AuthUser` and fetching as `const AuthUser` won't work.

View File

@@ -0,0 +1,51 @@
---
title: "S13. Return a Custom Error Page"
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.
## Basic usage
```cpp
svr.set_error_handler([](const httplib::Request &req, httplib::Response &res) {
auto body = "<h1>Error " + std::to_string(res.status) + "</h1>";
res.set_content(body, "text/html");
});
```
The error handler runs right before an error response is sent — any time `res.status` is 4xx or 5xx. Replace the body with `res.set_content()` and every error response uses the same template.
## Branch by status code
```cpp
svr.set_error_handler([](const httplib::Request &req, httplib::Response &res) {
if (res.status == 404) {
res.set_content("<h1>Not Found</h1><p>" + req.path + "</p>", "text/html");
} else if (res.status >= 500) {
res.set_content("<h1>Server Error</h1>", "text/html");
}
});
```
Checking `res.status` lets you show a custom message for 404s and a "contact support" link for 5xx errors.
## JSON error responses
For an API server, you probably want errors as JSON.
```cpp
svr.set_error_handler([](const httplib::Request &req, httplib::Response &res) {
nlohmann::json j = {
{"error", true},
{"status", res.status},
{"path", req.path},
};
res.set_content(j.dump(), "application/json");
});
```
Now every error comes back in a consistent JSON shape.
> **Note:** `set_error_handler()` also fires for 500 responses caused by exceptions thrown from a route handler. To get at the exception itself, combine it with `set_exception_handler()`. See S14. Catch Exceptions.

View File

@@ -0,0 +1,67 @@
---
title: "S14. Catch Exceptions"
order: 33
status: "draft"
---
When a route handler throws, cpp-httplib keeps the server running and responds with 500. By default, though, very little of the error information reaches the client. `set_exception_handler()` lets you intercept exceptions and build your own response.
## Basic usage
```cpp
svr.set_exception_handler(
[](const httplib::Request &req, httplib::Response &res,
std::exception_ptr ep) {
try {
std::rethrow_exception(ep);
} catch (const std::exception &e) {
res.status = 500;
res.set_content(std::string("error: ") + e.what(), "text/plain");
} catch (...) {
res.status = 500;
res.set_content("unknown error", "text/plain");
}
});
```
The handler receives a `std::exception_ptr`. The idiomatic move is to rethrow it with `std::rethrow_exception()` and catch by type. You can vary status code and message based on the exception type.
## Branch on custom exception types
If you throw your own exception types, you can map them to 400 or 404 responses.
```cpp
struct NotFound : std::runtime_error {
using std::runtime_error::runtime_error;
};
struct BadRequest : std::runtime_error {
using std::runtime_error::runtime_error;
};
svr.set_exception_handler(
[](const auto &req, auto &res, std::exception_ptr ep) {
try {
std::rethrow_exception(ep);
} catch (const NotFound &e) {
res.status = 404;
res.set_content(e.what(), "text/plain");
} catch (const BadRequest &e) {
res.status = 400;
res.set_content(e.what(), "text/plain");
} catch (const std::exception &e) {
res.status = 500;
res.set_content("internal error", "text/plain");
}
});
```
Now throwing `NotFound("user not found")` inside a handler is enough to return 404. No per-handler try/catch needed.
## Relationship with set_error_handler
`set_exception_handler()` runs the moment the exception is thrown. After that, if `res.status` is 4xx or 5xx, `set_error_handler()` also runs. The order is `exception_handler``error_handler`. Think of their roles as:
- **Exception handler**: interpret the exception, set the status and message
- **Error handler**: see the status and wrap it in the shared template
> **Note:** Without an exception handler, cpp-httplib returns a default 500 response and the exception details never make it to logs. Always set one for anything you want to debug.

View File

@@ -0,0 +1,64 @@
---
title: "S15. Log Requests on the Server"
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.
## Basic usage
```cpp
svr.set_logger([](const httplib::Request &req, const httplib::Response &res) {
std::cout << req.remote_addr << " "
<< req.method << " " << req.path
<< " -> " << res.status << std::endl;
});
```
The log callback receives both the `Request` and the `Response`. You can grab the method, path, status, client IP, headers, body — whatever you need.
## Access-log style format
Here's an Apache/Nginx-ish access log format.
```cpp
svr.set_logger([](const auto &req, const auto &res) {
auto now = std::time(nullptr);
char timebuf[32];
std::strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S",
std::localtime(&now));
std::cout << timebuf << " "
<< req.remote_addr << " "
<< "\"" << req.method << " " << req.path << "\" "
<< res.status << " "
<< res.body.size() << "B"
<< std::endl;
});
```
## 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.
```cpp
svr.set_pre_routing_handler([](const auto &req, auto &res) {
res.user_data.set("start", std::chrono::steady_clock::now());
return httplib::Server::HandlerResponse::Unhandled;
});
svr.set_logger([](const auto &req, const auto &res) {
auto *start = res.user_data.get<std::chrono::steady_clock::time_point>("start");
auto elapsed = start
? std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - *start).count()
: 0;
std::cout << req.method << " " << req.path
<< " " << res.status << " " << elapsed << "ms" << std::endl;
});
```
For more on `user_data`, see S12. Pass Data Between Handlers with `res.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.

View File

@@ -0,0 +1,55 @@
---
title: "S16. Detect When the Client Has Disconnected"
order: 35
status: "draft"
---
During a long-running response, the client might close the connection. There's no point continuing to do work no one's waiting for. In cpp-httplib, check `req.is_connection_closed()`.
## Basic usage
```cpp
svr.Get("/long-task", [](const httplib::Request &req, httplib::Response &res) {
for (int i = 0; i < 1000; ++i) {
if (req.is_connection_closed()) {
std::cout << "client disconnected" << std::endl;
return;
}
do_heavy_work(i);
}
res.set_content("done", "text/plain");
});
```
`is_connection_closed` is a `std::function<bool()>`, so call it with `()`. It returns `true` when the client is gone.
## With a streaming response
The same check works inside `set_chunked_content_provider()`. Capture the request by reference.
```cpp
svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
res.set_chunked_content_provider(
"text/event-stream",
[&req](size_t offset, httplib::DataSink &sink) {
if (req.is_connection_closed()) {
sink.done();
return true;
}
auto event = generate_next_event();
sink.write(event.data(), event.size());
return true;
});
});
```
When you detect a disconnect, call `sink.done()` to stop the provider from being called again.
## How often should you check?
The call itself is cheap, but calling it in a tight inner loop doesn't add much value. Check at **boundaries where interrupting is safe** — after producing a chunk, after a database query, etc.
> **Warning:** `is_connection_closed()` is not guaranteed to reflect reality instantly. Because of how TCP works, sometimes you only notice the disconnect when you try to send. Don't expect pixel-perfect real-time detection — think of it as "we'll notice eventually."

View File

@@ -0,0 +1,52 @@
---
title: "S17. Bind to Any Available Port"
order: 36
status: "draft"
---
Standing up a test server often hits port conflicts. With `bind_to_any_port()`, you let the OS pick a free port and then read back which one it gave you.
## Basic usage
```cpp
httplib::Server svr;
svr.Get("/", [](const auto &req, auto &res) {
res.set_content("hello", "text/plain");
});
int port = svr.bind_to_any_port("0.0.0.0");
std::cout << "listening on port " << port << std::endl;
svr.listen_after_bind();
```
`bind_to_any_port()` is equivalent to passing `0` as the port — the OS assigns a free one. The return value is the port actually used.
After that, call `listen_after_bind()` to start accepting. You can't combine bind and listen into a single call here, so you work in two steps.
## Useful in tests
This pattern is great for tests that spin up a server and hit it.
```cpp
httplib::Server svr;
svr.Get("/ping", [](const auto &, auto &res) { res.set_content("pong", "text/plain"); });
int port = svr.bind_to_any_port("127.0.0.1");
std::thread t([&] { svr.listen_after_bind(); });
// run the test while the server is up on another thread
httplib::Client cli("127.0.0.1", port);
auto res = cli.Get("/ping");
assert(res && res->body == "pong");
svr.stop();
t.join();
```
Because the port is assigned at runtime, parallel test runs don't collide.
> **Note:** `bind_to_any_port()` returns `-1` on failure (permission errors, no available ports, etc.). Always check the return value.
> To stop the server, see S19. Shut Down Gracefully.

View File

@@ -0,0 +1,57 @@
---
title: "S18. Control Startup Order with listen_after_bind"
order: 37
status: "draft"
---
Normally `svr.listen("0.0.0.0", 8080)` handles bind and listen in one shot. When you need to do something between the two, split them into two calls.
## Separate bind and listen
```cpp
httplib::Server svr;
svr.Get("/", [](const auto &, auto &res) { res.set_content("ok", "text/plain"); });
if (!svr.bind_to_port("0.0.0.0", 8080)) {
std::cerr << "bind failed" << std::endl;
return 1;
}
// bind is done here. accept hasn't started yet.
drop_privileges();
signal_ready_to_parent_process();
svr.listen_after_bind(); // start the accept loop
```
`bind_to_port()` reserves the port; `listen_after_bind()` actually starts accepting. Splitting them gives you a window between the two steps.
## Common use cases
**Privilege drop**: Binding to a port under 1024 requires root. Bind as root, drop to a normal user, and all subsequent request handling runs with reduced privileges.
```cpp
svr.bind_to_port("0.0.0.0", 80);
drop_privileges();
svr.listen_after_bind();
```
**Startup notification**: Tell the parent process or systemd "I'm ready" before starting to accept connections.
**Test synchronization**: In tests, you can reliably catch "the moment the server is bound" and start the client after that.
## Check the return values
`bind_to_port()` returns `false` on failure — typically when the port is already taken. Always check it.
```cpp
if (!svr.bind_to_port("0.0.0.0", 8080)) {
std::cerr << "port already in use" << std::endl;
return 1;
}
```
`listen_after_bind()` blocks until the server stops and returns `true` on a clean shutdown.
> **Note:** To auto-pick a free port, see S17. Bind to Any Available Port. Under the hood, that's just `bind_to_any_port()` + `listen_after_bind()`.

View File

@@ -0,0 +1,57 @@
---
title: "S19. Shut Down Gracefully"
order: 38
status: "draft"
---
To stop the server, call `Server::stop()`. It's safe to call even while requests are in flight, so you can wire it to SIGINT or SIGTERM for a graceful shutdown.
## Basic usage
```cpp
httplib::Server svr;
svr.Get("/", [](const auto &, auto &res) { res.set_content("ok", "text/plain"); });
std::thread t([&] { svr.listen("0.0.0.0", 8080); });
// wait for input on the main thread, or whatever
std::cin.get();
svr.stop();
t.join();
```
`listen()` blocks, so the typical pattern is: run the server on a background thread and call `stop()` from the main thread. After `stop()`, `listen()` returns and you can `join()`.
## Shut down on a signal
Here's how to stop the server on SIGINT (Ctrl+C) or SIGTERM.
```cpp
#include <csignal>
httplib::Server svr;
// global so the signal handler can reach it
httplib::Server *g_svr = nullptr;
int main() {
svr.Get("/", [](const auto &, auto &res) { res.set_content("ok", "text/plain"); });
g_svr = &svr;
std::signal(SIGINT, [](int) { if (g_svr) g_svr->stop(); });
std::signal(SIGTERM, [](int) { if (g_svr) g_svr->stop(); });
svr.listen("0.0.0.0", 8080);
std::cout << "server stopped" << std::endl;
}
```
`stop()` is thread-safe and signal-safe — you can call it from a signal handler. Even when `listen()` is running on the main thread, the signal pulls it out cleanly.
## What happens to in-flight requests
When you call `stop()`, new connections are refused, but requests already being processed are **allowed to finish**. 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.

View File

@@ -0,0 +1,57 @@
---
title: "S20. Tune Keep-Alive"
order: 39
status: "draft"
---
`httplib::Server` enables HTTP/1.1 Keep-Alive automatically. From the client's perspective, connections are reused — so they don't pay the TCP handshake cost on every request. When you need to tune the behavior, there are two setters.
## What you can configure
| API | Default | Meaning |
| --- | --- | --- |
| `set_keep_alive_max_count` | 100 | Max requests served over a single connection |
| `set_keep_alive_timeout` | 5s | How long an idle connection is kept before closing |
## Basic usage
```cpp
httplib::Server svr;
svr.set_keep_alive_max_count(20);
svr.set_keep_alive_timeout(10); // 10 seconds
svr.listen("0.0.0.0", 8080);
```
`set_keep_alive_timeout()` also has a `std::chrono` overload.
```cpp
using namespace std::chrono_literals;
svr.set_keep_alive_timeout(10s);
```
## Tuning ideas
**Too many idle connections eating resources**
Shorten the timeout so idle connections drop and release their worker threads.
```cpp
svr.set_keep_alive_timeout(2s);
```
**API is hammered and you want max reuse**
Raising the per-connection request cap improves benchmark numbers.
```cpp
svr.set_keep_alive_max_count(1000);
```
**Never reuse connections**
Set `set_keep_alive_max_count(1)` and every request gets its own connection. Mostly only useful for debugging or compatibility testing.
## 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.
> **Note:** For the client side, see C14. Understand Connection Reuse and Keep-Alive Behavior. Even when the server closes the connection on timeout, the client reconnects automatically.

View File

@@ -0,0 +1,69 @@
---
title: "S21. Configure the Thread Pool"
order: 40
status: "draft"
---
cpp-httplib serves requests from a thread pool. By default, the base thread count is the greater of `std::thread::hardware_concurrency() - 1` and `8`, and it can scale up dynamically to 4× that. To set thread counts explicitly, provide your own factory via `new_task_queue`.
## Set thread counts
```cpp
httplib::Server svr;
svr.new_task_queue = [] {
return new httplib::ThreadPool(/*base_threads=*/8, /*max_threads=*/64);
};
svr.listen("0.0.0.0", 8080);
```
The factory is a lambda returning a `TaskQueue*`. Pass `base_threads` and `max_threads` to `ThreadPool` and the pool scales between them based on load. Idle threads exit after a timeout (3 seconds by default).
## Also cap the queue
The pending queue can eat memory if it grows unchecked. You can cap it too.
```cpp
svr.new_task_queue = [] {
return new httplib::ThreadPool(
/*base_threads=*/12,
/*max_threads=*/0, // disable dynamic scaling
/*max_queued_requests=*/18);
};
```
`max_threads=0` disables dynamic scaling — you get a fixed `base_threads`. Requests that don't fit in `max_queued_requests` are rejected.
## Use your own thread pool
You can plug in a fully custom thread pool by subclassing `TaskQueue` and returning it from the factory.
```cpp
class MyTaskQueue : public httplib::TaskQueue {
public:
MyTaskQueue(size_t n) { pool_.start_with_thread_count(n); }
bool enqueue(std::function<void()> fn) override { return pool_.post(std::move(fn)); }
void shutdown() override { pool_.shutdown(); }
private:
MyThreadPool pool_;
};
svr.new_task_queue = [] { return new MyTaskQueue(12); };
```
Handy when you already have a thread pool in your project and want to keep thread management unified.
## Compile-time tuning
You can set the defaults with macros if you want compile-time configuration.
```cpp
#define CPPHTTPLIB_THREAD_POOL_COUNT 16 // base thread count
#define CPPHTTPLIB_THREAD_POOL_MAX_COUNT 128 // max thread count
#define CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT 5 // seconds before idle threads exit
#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)`).

View File

@@ -0,0 +1,64 @@
---
title: "S22. Talk Over a Unix Domain Socket"
order: 41
status: "draft"
---
When you want to talk only to other processes on the same host, a Unix domain socket is a nice fit. It avoids TCP overhead and uses filesystem permissions for access control. Local IPC and services sitting behind a reverse proxy are classic use cases.
## Server side
```cpp
httplib::Server svr;
svr.set_address_family(AF_UNIX);
svr.Get("/", [](const auto &, auto &res) {
res.set_content("hello from unix socket", "text/plain");
});
svr.listen("/tmp/httplib.sock", 80);
```
Call `set_address_family(AF_UNIX)` first, then pass the socket file path as the first argument to `listen()`. The port number is unused but required by the signature — pass any value.
## Client side
```cpp
httplib::Client cli("/tmp/httplib.sock");
cli.set_address_family(AF_UNIX);
auto res = cli.Get("/");
if (res) {
std::cout << res->body << std::endl;
}
```
Pass the socket file path to the `Client` constructor and call `set_address_family(AF_UNIX)`. Everything else works like a normal HTTP request.
## When to use it
- **Behind a reverse proxy**: An nginx-to-backend setup over a Unix socket is faster than TCP and sidesteps port management
- **Local-only APIs**: IPC between tools that shouldn't be reachable from outside
- **In-container IPC**: Process-to-process communication within the same pod or container
- **Dev environments**: No more worrying about port conflicts
## Clean up the socket file
A Unix domain socket creates a real file in the filesystem. It doesn't get removed on shutdown, so delete it before starting if needed.
```cpp
std::remove("/tmp/httplib.sock");
svr.listen("/tmp/httplib.sock", 80);
```
## Permissions
You control who can connect via the socket file's permissions.
```cpp
svr.listen("/tmp/httplib.sock", 80);
// from another process or thread
chmod("/tmp/httplib.sock", 0660); // owner and group only
```
> **Warning:** Some Windows versions support AF_UNIX, but the implementation and behavior differ by platform. Test thoroughly before running cross-platform in production.