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 @@ status: "draft"
## サーバー
### 基本
- S01. GET / POST / PUT / DELETEハンドラを登録する
- S02. JSONリクエストを受け取りJSONレスポンスを返す
- S03. パスパラメーターを使う`/users/:id`
- S04. 静的ファイルサーバーを設定する`set_mount_point`
- [S01. GET / POST / PUT / DELETEハンドラを登録する](s01-handlers)
- [S02. JSONリクエストを受け取りJSONレスポンスを返す](s02-json-api)
- [S03. パスパラメーターを使う](s03-path-params)
- [S04. 静的ファイルサーバーを設定する](s04-static-files)
### ストリーミング・ファイル
- S05. 大きなファイルをストリーミングで返す`ContentProvider`
- S06. ファイルダウンロードレスポンスを返す`Content-Disposition`
- S07. マルチパートデータをストリーミングで受け取る`ContentReader`
- S08. レスポンスを圧縮して返すgzip
- [S05. 大きなファイルをストリーミングで返す](s05-stream-response)
- [S06. ファイルダウンロードレスポンスを返す](s06-download-response)
- [S07. マルチパートデータをストリーミングで受け取る](s07-multipart-reader)
- [S08. レスポンスを圧縮して返す](s08-compress-response)
### ハンドラチェーン
- S09. 全ルートに共通の前処理をするPre-routing handler
- S10. Post-routing handlerでレスポンスヘッダーを追加するCORSなど
- S11. Pre-request handlerでルート単位の認証を行う`matched_route`
- S12. `res.user_data`でハンドラ間データを渡す
- [S09. 全ルートに共通の前処理をする](s09-pre-routing)
- [S10. Post-routing handlerでレスポンスヘッダーを追加する](s10-post-routing)
- [S11. Pre-request handlerでルート単位の認証を行う](s11-pre-request)
- [S12. `res.user_data`でハンドラ間データを渡す](s12-user-data)
### エラー処理・デバッグ
- S13. カスタムエラーページを返す`set_error_handler`
- S14. 例外をキャッチする`set_exception_handler`
- S15. リクエストをログに記録するLogger
- S16. クライアントが切断したか検出する`req.is_connection_closed()`
- [S13. カスタムエラーページを返す](s13-error-handler)
- [S14. 例外をキャッチする](s14-exception-handler)
- [S15. リクエストをログに記録する](s15-server-logger)
- [S16. クライアントが切断したか検出する](s16-disconnect)
### 運用・チューニング
- S17. ポートを動的に割り当てる`bind_to_any_port`
- S18. `listen_after_bind`で起動順序を制御する
- S19. グレースフルシャットダウンする`stop()`とシグナルハンドリング)
- S20. Keep-Aliveを調整する`set_keep_alive_max_count` / `set_keep_alive_timeout`
- S21. マルチスレッド数を設定する`new_task_queue`
- S22. Unix domain socketで通信する`set_address_family(AF_UNIX)`
- [S17. ポートを動的に割り当てる](s17-bind-any-port)
- [S18. `listen_after_bind`で起動順序を制御する](s18-listen-after-bind)
- [S19. グレースフルシャットダウンする](s19-graceful-shutdown)
- [S20. Keep-Aliveを調整する](s20-keep-alive)
- [S21. マルチスレッド数を設定する](s21-thread-pool)
- [S22. Unix domain socketで通信する](s22-unix-socket)
## TLS / セキュリティ

View File

@@ -0,0 +1,66 @@
---
title: "S01. GET / POST / PUT / DELETEハンドラを登録する"
order: 20
status: "draft"
---
`httplib::Server`では、HTTPメソッドごとにハンドラを登録します。`Get()``Post()``Put()``Delete()`の各メソッドにパターンとラムダを渡すだけです。
## 基本の使い方
```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にリクエストボディが入っている
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);
}
```
ハンドラは`(const Request&, Response&)`の2引数を受け取ります。`res.set_content()`でレスポンスボディとContent-Typeを設定し、`res.status`でステータスコードを指定します。`listen()`を呼ぶとサーバーが起動し、ブロックされます。
## クエリパラメーターを取得する
```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()`でクエリ文字列の値を取り出せます。存在するかどうかを先に調べたいなら`req.has_param("q")`を使います。
## リクエストヘッダーを読む
```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");
});
```
レスポンスヘッダーを追加したいときは`res.set_header("Name", "Value")`です。
> **Note:** `listen()`はブロックする関数です。別スレッドで動かしたいときは`std::thread`で包むか、ンブロッキング起動が必要ならS18. `listen_after_bind`で起動順序を制御するを参照してください。
> パスパラメーター(`/users/:id`を使いたい場合はS03. パスパラメーターを使うを参照してください。

View File

@@ -0,0 +1,74 @@
---
title: "S02. JSONリクエストを受け取りJSONレスポンスを返す"
order: 21
status: "draft"
---
cpp-httplibにはJSONパーサーが含まれていません。サーバー側でも[nlohmann/json](https://github.com/nlohmann/json)などを組み合わせて使います。ここでは`nlohmann/json`を例に説明します。
## 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`はそのまま`std::string`なので、JSONライブラリに渡してパースします。レスポンスは`dump()`で文字列にして、Content-Typeに`application/json`を指定して返します。
## 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;
}
// ...
});
```
厳密にJSONだけを受け付けたいときは、Content-Typeを確認してから処理しましょう。
## JSONを返すヘルパーを作る
同じパターンを何度も書くなら、小さなヘルパーを用意すると楽です。
```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:** 大きなJSONボディを受け取ると、`req.body`がまるごとメモリに載ります。巨大なペイロードを扱うときはS07. マルチパートデータをストリーミングで受け取るのように、ストリーミング受信も検討しましょう。
> クライアント側の書き方はC02. JSONを送受信するを参照してください。

View File

@@ -0,0 +1,53 @@
---
title: "S03. パスパラメーターを使う"
order: 22
status: "draft"
---
REST APIでよく使う`/users/:id`のような動的なパスは、パスパターンに`:name`を書くだけで使えます。マッチした値は`req.path_params`に入ります。
## 基本の使い方
```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");
});
```
`/users/42`にアクセスすると、`req.path_params["id"]``"42"`が入ります。`path_params``std::unordered_map<std::string, std::string>`なので、`at()`で取り出します。
## 複数のパラメーター
パラメーターはいくつでも書けます。
```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");
});
```
`/orgs/anthropic/repos/cpp-httplib`のようなパスがマッチします。
## 正規表現パターン
もっと柔軟にマッチさせたいときは、`std::regex`ベースのパターンも使えます。
```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");
});
```
パターンに括弧を使うと、マッチした部分が`req.matches`に入ります。`req.matches[0]`はパス全体、`req.matches[1]`以降がキャプチャです。
## どちらを使うか
- 単純なIDやスラッグなら`:name`でじゅうぶん。読みやすく、型が自明です
- 数値のみ、UUIDのみといった形式をURLで絞りたいなら正規表現が便利
- 両方を混ぜると混乱するので、プロジェクト内ではどちらかに統一するのがおすすめです
> **Note:** パスパラメーターは文字列として入ってくるので、整数として使いたい場合は`std::stoi()`などで変換してください。変換失敗のハンドリングも忘れずに。

View File

@@ -0,0 +1,55 @@
---
title: "S04. 静的ファイルサーバーを設定する"
order: 23
status: "draft"
---
HTML、CSS、画像などの静的ファイルを配信したいときは、`set_mount_point()`を使います。URLパスとローカルディレクトリを結びつけるだけで、そのディレクトリの中身がまるごと配信されます。
## 基本の使い方
```cpp
httplib::Server svr;
svr.set_mount_point("/", "./public");
svr.listen("0.0.0.0", 8080);
```
`./public/index.html``http://localhost:8080/index.html`で、`./public/css/style.css``http://localhost:8080/css/style.css`でアクセスできます。ディレクトリ構造がそのままURLに反映されます。
## 複数のマウントポイント
マウントポイントは複数登録できます。
```cpp
svr.set_mount_point("/", "./public");
svr.set_mount_point("/assets", "./dist/assets");
svr.set_mount_point("/uploads", "./var/uploads");
```
同じパスに複数のマウントを登録することもできます。その場合は登録順に探されて、見つかった最初のものが返ります。
## APIハンドラと組み合わせる
静的ファイルとAPIハンドラは共存できます。`Get()`などで登録したハンドラが優先され、マッチしなかったときにマウントポイントが探されます。
```cpp
svr.Get("/api/users", [](const auto &req, auto &res) {
res.set_content("[]", "application/json");
});
svr.set_mount_point("/", "./public");
```
これでSPAのように、`/api/*`はハンドラで、それ以外は`./public/`から配信、という構成が作れます。
## MIMEタイプを追加する
拡張子からContent-Typeを決めるマッピングは組み込みですが、カスタムの拡張子を追加できます。
```cpp
svr.set_file_extension_and_mimetype_mapping("wasm", "application/wasm");
```
> **Warning:** 静的ファイル配信系のメソッドは**スレッドセーフではありません**。起動後(`listen()`以降)には呼ばないでください。起動前にまとめて設定しましょう。
> ダウンロード用のレスポンスを返したい場合はS06. ファイルダウンロードレスポンスを返すも参考になります。

View File

@@ -0,0 +1,63 @@
---
title: "S05. 大きなファイルをストリーミングで返す"
order: 24
status: "draft"
---
巨大なファイルやリアルタイムに生成されるデータをレスポンスとして返したいとき、全体をメモリに載せるのは現実的ではありません。`Response::set_content_provider()`を使うと、データをチャンクごとに生成しながら送れます。
## サイズがわかっている場合
```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;
});
});
```
ラムダが呼ばれるたびに`offset``length`が渡されるので、その範囲だけ読み込んで`sink.write()`で送ります。メモリには常に少量のチャンクしか載りません。
## ファイルをそのまま返す
ただファイルを返すだけなら、`set_file_content()`のほうがずっと簡単です。
```cpp
svr.Get("/download", [](const httplib::Request &req, httplib::Response &res) {
res.set_file_content("large.bin", "application/octet-stream");
});
```
内部でストリーミング送信をしてくれるので、大きなファイルでも安心です。Content-Typeを省略すれば、拡張子から自動で判定されます。
## サイズが不明な場合はチャンク転送
リアルタイムに生成されるデータなど、サイズが事前にわからないときは`set_chunked_content_provider()`を使います。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(); // 送信終了
return true;
}
sink.write(chunk.data(), chunk.size());
return true;
});
});
```
データがもう無くなったら`sink.done()`を呼んで終了します。
> **Note:** プロバイダラムダは複数回呼ばれます。キャプチャする変数のライフタイムに気をつけてください。必要なら`std::shared_ptr`などで包みましょう。
> ファイルダウンロードとして扱いたい場合はS06. ファイルダウンロードレスポンスを返すを参照してください。

View File

@@ -0,0 +1,50 @@
---
title: "S06. ファイルダウンロードレスポンスを返す"
order: 25
status: "draft"
---
ブラウザで開いたときにインラインで表示するのではなく、**ダウンロードダイアログ**を出したいときは、`Content-Disposition`ヘッダーを付けます。cpp-httplib側の特別なAPIではなく、普通のヘッダー設定で実現します。
## 基本の使い方
```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`を付けると、ブラウザが「保存しますか?」のダイアログを出します。`filename=`で保存時のデフォルト名を指定できます。
## 日本語など非ASCIIのファイル名
ファイル名に日本語やスペースが含まれる場合は、RFC 5987形式の`filename*`を使います。
```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");
});
```
`filename*=UTF-8''`の後ろはURLエンコード済みのUTF-8バイト列です。古いブラウザ向けにASCIIの`filename=`も併記しておくと安全です。
## 動的に生成したデータをダウンロードさせる
ファイルがなくても、生成した文字列をそのままダウンロードさせることもできます。
```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");
});
```
CSVエクスポートなどでよく使うパターンです。
> **Note:** ブラウザによっては`Content-Disposition`がなくても、Content-Typeを見て自動でダウンロード扱いにすることがあります。逆に、`inline`を付けるとできるだけブラウザ内で表示しようとします。

View File

@@ -0,0 +1,71 @@
---
title: "S07. マルチパートデータをストリーミングで受け取る"
order: 26
status: "draft"
---
大きなファイルをアップロードするハンドラを普通に書くと、`req.body`にリクエスト全体が載ってしまいメモリを圧迫します。`HandlerWithContentReader`を使うと、ボディをチャンクごとに受け取れます。
## 基本の使い方
```cpp
svr.Post("/upload",
[](const httplib::Request &req, httplib::Response &res,
const httplib::ContentReader &content_reader) {
if (req.is_multipart_form_data()) {
content_reader(
// 各パートのヘッダー
[&](const httplib::FormData &file) {
std::cout << "name: " << file.name
<< ", filename: " << file.filename << std::endl;
return true;
},
// 各パートのボディ(複数回呼ばれる)
[&](const char *data, size_t len) {
// ここでファイルに書き出すなど
return true;
});
} else {
// 普通のリクエストボディ
content_reader([&](const char *data, size_t len) {
return true;
});
}
res.set_content("ok", "text/plain");
});
```
`content_reader`は2通りの呼び方ができます。マルチパートのときは2つのコールバックヘッダー用とデータ用を渡し、そうでないときは1つのコールバックだけを渡します。
## ファイルに直接書き出す
大きなファイルをそのままディスクに書き出す例です。
```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");
});
```
メモリには常に小さなチャンクしか載らないので、ギガバイト級のファイルでも扱えます。
> **Warning:** `HandlerWithContentReader`を使うと、`req.body`は**空のまま**です。ボディはコールバック内で自分で処理してください。
> クライアント側でマルチパートを送る方法はC07. ファイルをマルチパートフォームとしてアップロードするを参照してください。

View File

@@ -0,0 +1,53 @@
---
title: "S08. レスポンスを圧縮して返す"
order: 27
status: "draft"
---
cpp-httplibは、クライアントが`Accept-Encoding`で対応を表明していれば、レスポンスボディを自動で圧縮してくれます。ハンドラ側で特別なことをする必要はありません。対応しているのはgzip、Brotli、Zstdです。
## ビルド時の準備
圧縮機能を使うには、`httplib.h`をインクルードする前に対応するマクロを定義しておきます。
```cpp
#define CPPHTTPLIB_ZLIB_SUPPORT // gzip
#define CPPHTTPLIB_BROTLI_SUPPORT // brotli
#define CPPHTTPLIB_ZSTD_SUPPORT // zstd
#include <httplib.h>
```
それぞれ`zlib``brotli``zstd`をリンクする必要があります。必要な圧縮方式だけ有効にすればOKです。
## 使い方
```cpp
svr.Get("/api/data", [](const httplib::Request &req, httplib::Response &res) {
std::string body = build_large_response();
res.set_content(body, "application/json");
});
```
これだけです。クライアントが`Accept-Encoding: gzip`を送ってきていれば、cpp-httplibが自動でgzip圧縮して返します。レスポンスには`Content-Encoding: gzip``Vary: Accept-Encoding`が自動で付きます。
## 圧縮の優先順位
クライアントが複数の方式を受け入れる場合、Brotli → Zstd → gzipの順に選ばれますビルドで有効になっている中から。クライアント側では気にせず、一番効率の良い方式で圧縮されます。
## ストリーミングレスポンスも圧縮される
`set_chunked_content_provider()`で返すストリーミングレスポンスも、同じように自動で圧縮されます。
```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:** 小さなレスポンスは圧縮しても効果が薄く、むしろCPU時間を無駄にすることがあります。cpp-httplibは小さすぎるボディは圧縮をスキップします。
> クライアント側の挙動はC15. 圧縮を有効にするを参照してください。

View File

@@ -0,0 +1,54 @@
---
title: "S09. 全ルートに共通の前処理をする"
order: 28
status: "draft"
---
すべてのリクエストに対して共通の処理を走らせたいことがあります。認証チェック、ロギング、レート制限などです。こうした処理は`set_pre_routing_handler()`で登録します。
## 基本の使い方
```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;
});
```
Pre-routingハンドラは、**ルーティングよりも前**に呼ばれます。どのハンドラにもマッチしないリクエストも含めて、すべてのリクエストを捕まえられます。
戻り値の`HandlerResponse`がポイントです。
- `Unhandled`を返す: 通常の処理を続行(ルーティングとハンドラ呼び出し)
- `Handled`を返す: ここでレスポンスが完了したとみなし、以降の処理をスキップ
## 認証チェックに使う
全ルート共通の認証を一箇所でかけられます。
```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; // 認証不要
}
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;
});
```
認証が通らなければ`Handled`を返してその場で401を返し、通れば`Unhandled`を返して通常のルーティングに進ませます。
## 特定ルートだけに認証をかけたい場合
全ルート共通ではなく、ルート単位で認証を分けたいときは、S11. Pre-request handlerでルート単位の認証を行うのほうが適しています。
> **Note:** レスポンスを加工したいだけなら、`set_post_routing_handler()`のほうが適切です。S10. Post-routing handlerでレスポンスヘッダーを追加するを参照してください。

View File

@@ -0,0 +1,56 @@
---
title: "S10. Post-routing handlerでレスポンスヘッダーを追加する"
order: 29
status: "draft"
---
ハンドラが返したレスポンスに、あとから共通のヘッダーを追加したいことがあります。CORSヘッダー、セキュリティヘッダー、独自のリクエストIDなどです。こういうときは`set_post_routing_handler()`を使います。
## 基本の使い方
```cpp
svr.set_post_routing_handler(
[](const httplib::Request &req, httplib::Response &res) {
res.set_header("X-Request-ID", generate_request_id());
});
```
Post-routingハンドラは、**ルートハンドラが実行された後、レスポンスが送信される前**に呼ばれます。ここで`res.set_header()``res.headers.erase()`を使えば、全レスポンスに対して一括でヘッダーの追加・削除ができます。
## CORSヘッダーを付ける
よくある用途がCORSです。
```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");
});
```
プリフライトリクエスト(`OPTIONS`には別途ハンドラを登録するか、pre-routingハンドラで処理します。
```cpp
svr.Options("/.*", [](const auto &req, auto &res) {
res.status = 204;
});
```
## セキュリティヘッダーをまとめて付ける
ブラウザ向けのセキュリティヘッダーを一箇所で管理できます。
```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");
});
```
どのハンドラがレスポンスを作っても、同じヘッダーが付くようになります。
> **Note:** Post-routingハンドラは、ルートにマッチしなかったリクエストや、エラーハンドラが返したレスポンスに対しても呼ばれます。ヘッダーをすべてのレスポンスに確実に付けたいときに便利です。

View File

@@ -0,0 +1,47 @@
---
title: "S11. Pre-request handlerでルート単位の認証を行う"
order: 30
status: "draft"
---
S09で紹介した`set_pre_routing_handler()`はルーティングの**前**に呼ばれるので、「どのルートにマッチしたか」を知れません。ルートによって認証の有無を変えたい場合は、`set_pre_request_handler()`のほうが便利です。
## Pre-routingとの違い
| フック | 呼ばれるタイミング | ルート情報 |
| --- | --- | --- |
| `set_pre_routing_handler` | ルーティングの前 | 取得できない |
| `set_pre_request_handler` | ルーティング後、ルートハンドラの直前 | `req.matched_route`で取得可能 |
Pre-requestハンドラなら、`req.matched_route`に「マッチしたパターン文字列」が入っているので、ルートに応じて処理を変えられます。
## ルートごとに認証を切り替える
```cpp
svr.set_pre_request_handler(
[](const httplib::Request &req, httplib::Response &res) {
// /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`はパスパラメーターを展開する**前**のパターン文字列(例: `/admin/users/:id`です。特定の値ではなく、ルート定義のパターンで判定できるので、IDや名前に左右されません。
## 戻り値の意味
Pre-routingハンドラと同じく、`HandlerResponse`を返します。
- `Unhandled`: 通常の処理を続行(ルートハンドラが呼ばれる)
- `Handled`: ここで完了、ルートハンドラはスキップされる
## 認証情報を後続のハンドラに渡す
認証で取り出したユーザー情報などをルートハンドラに渡したいときは、`res.user_data`を使います。詳しくはS12. `res.user_data`でハンドラ間データを渡すを参照してください。

View File

@@ -0,0 +1,56 @@
---
title: "S12. res.user_dataでハンドラ間データを渡す"
order: 31
status: "draft"
---
Pre-requestハンドラで認証トークンをデコードして、その結果をルートハンドラで使いたい。こういう「ハンドラ間のデータ受け渡し」は、`res.user_data`に任意の型を入れて解決します。
## 基本の使い方
```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); // 認証トークンをデコード
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()`で任意の型の値を保存し、`user_data.get<T>()`で取り出します。型を正しく指定しないと`nullptr`が返るので注意してください。
## よくある型
`std::string`、数値、構造体、`std::shared_ptr`など、コピーかムーブできる値なら何でも入れられます。
```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());
```
## どこで設定し、どこで読むか
設定する側は`set_pre_routing_handler()``set_pre_request_handler()`、読む側は通常のルートハンドラ、という流れが一般的です。Pre-requestのほうがルーティング後に呼ばれるので、`req.matched_route`と組み合わせて「このルートにマッチしたときだけセット」という書き方ができます。
## 注意点
`user_data``Response`に乗っています(`req.user_data`ではありません)。これは、ハンドラには`Response&`として可変参照が渡されるためです。一見不思議ですが、「ハンドラ間で共有する可変コンテキスト」として覚えておくと素直です。
> **Warning:** `user_data.get<T>()`は型が一致しないと`nullptr`を返します。保存時と取得時で同じ型を指定してください。`AuthUser`で入れて`const AuthUser`で取ろうとすると失敗します。

View File

@@ -0,0 +1,51 @@
---
title: "S13. カスタムエラーページを返す"
order: 32
status: "draft"
---
404や500のような**ハンドラが返したエラーレスポンス**を加工したいときは、`set_error_handler()`を使います。デフォルトの味気ないエラーページを、独自のHTMLやJSONに差し替えられます。
## 基本の使い方
```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");
});
```
エラーハンドラは、`res.status`が4xxまたは5xxでレスポンスが返る直前に呼ばれます。`res.set_content()`で差し替えれば、すべてのエラーレスポンスで同じテンプレートが使えます。
## ステータスコード別の処理
```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");
}
});
```
`res.status`を見て分岐すれば、404には専用のメッセージを、5xxにはサポート窓口のリンクを、といった使い分けができます。
## JSON APIのエラーレスポンス
APIサーバーなら、エラーも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");
});
```
これで全エラーが統一されたJSONで返ります。
> **Note:** `set_error_handler()`は、ルートハンドラが例外を投げた場合の500エラーにも呼ばれます。例外そのものの情報を取り出したい場合は`set_exception_handler()`を組み合わせましょう。S14. 例外をキャッチするを参照してください。

View File

@@ -0,0 +1,67 @@
---
title: "S14. 例外をキャッチする"
order: 33
status: "draft"
---
ルートハンドラの中で例外が投げられても、cpp-httplibはサーバー全体を落とさずに500を返してくれます。ただ、デフォルトではエラー情報をクライアントにほとんど伝えません。`set_exception_handler()`を使うと、例外をキャッチして独自のレスポンスを組み立てられます。
## 基本の使い方
```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");
}
});
```
ハンドラは`std::exception_ptr`を受け取るので、いったん`std::rethrow_exception()`で投げ直してから`catch`でキャッチするのが定石です。例外の型によってステータスコードやメッセージを変えられます。
## 自前の例外型で分岐する
独自の例外クラスを投げている場合、それを見て400や404にマッピングできます。
```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");
}
});
```
ルートハンドラの中で`throw NotFound("user not found")`を投げるだけで、404が返るようになります。try/catchをハンドラごとに書かずに済むので、コードがすっきりします。
## set_error_handlerとの関係
`set_exception_handler()`は例外が発生した瞬間に呼ばれます。その後、`res.status`が4xx/5xxなら`set_error_handler()`も呼ばれます。順番は`exception_handler``error_handler`です。役割分担は次のとおりです。
- **例外ハンドラ**: 例外を解釈してステータスとメッセージを決める
- **エラーハンドラ**: 決まったステータスを見て、共通のテンプレートに整形する
> **Note:** 例外ハンドラを設定しないと、cpp-httplibはデフォルトの500レスポンスを返します。例外情報はログに残らないので、デバッグしたいなら必ず設定しましょう。

View File

@@ -0,0 +1,64 @@
---
title: "S15. リクエストをログに記録する"
order: 34
status: "draft"
---
サーバーが受け取ったリクエストと返したレスポンスをログに残したいときは、`Server::set_logger()`を使います。各リクエストの処理が完了するたびに呼ばれるので、アクセスログやメトリクス収集の土台になります。
## 基本の使い方
```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;
});
```
ログコールバックには`Request``Response`が渡ります。メソッド、パス、ステータスコード、クライアントIP、ヘッダー、ボディなど、好きな情報を取り出せます。
## アクセスログ風のフォーマット
Apache / Nginxのアクセスログに似た形式で残す例です。
```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;
});
```
## 処理時間を測る
ログで処理時間を出したいときは、pre-routingハンドラで開始時刻を`res.user_data`に保存しておき、ロガーで差分を取ります。
```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;
});
```
`user_data`の使い方はS12. `res.user_data`でハンドラ間データを渡すも参照してください。
> **Note:** ロガーはリクエスト処理と同じスレッドで同期的に呼ばれます。重い処理を直接入れると全体のスループットが落ちるので、必要ならキューに流して非同期で処理しましょう。

View File

@@ -0,0 +1,55 @@
---
title: "S16. クライアントが切断したか検出する"
order: 35
status: "draft"
---
長時間のレスポンスを返している最中に、クライアントが接続を切ってしまうことがあります。無駄に処理を続けても意味がないので、適宜チェックして中断できるようにしておきましょう。cpp-httplibでは`req.is_connection_closed()`で確認できます。
## 基本の使い方
```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()``std::function<bool()>`なので、`()`を付けて呼び出します。切断されていれば`true`を返します。
## ストリーミングレスポンスと組み合わせる
`set_chunked_content_provider()`でストリーミング配信しているときも、同じ方法で切断を検出できます。ラムダにキャプチャしておくと便利です。
```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;
});
});
```
切断を検出したら`sink.done()`を呼んで、プロバイダの呼び出しを止めます。
## どのくらいの頻度でチェックすべきか
毎ループで呼んでも軽い処理ですが、あまりに細かい単位で呼ぶと意味が薄れます。「1チャンクを生成し終えたタイミング」や「データベースの1クエリが終わったタイミング」など、**中断しても安全な境目**で確認するのが現実的です。
> **Warning:** `is_connection_closed()`の確認は即座に正確な値を返すとは限りません。TCPの特性上、送信が止まって初めて切断に気付くことも多いです。完璧なリアルタイム検出を期待せず、「そのうち気付ければいい」くらいの気持ちで使いましょう。

View File

@@ -0,0 +1,52 @@
---
title: "S17. ポートを動的に割り当てる"
order: 36
status: "draft"
---
テスト用サーバーを立てるとき、ポート番号の衝突が面倒なことがあります。`bind_to_any_port()`を使うと、OSに空いているポートを選ばせて、そのポート番号を受け取れます。
## 基本の使い方
```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()`は第2引数で`0`を渡したのと同じ動作で、OSが空きポートを割り当てます。返り値が実際に使われたポート番号です。
その後、`listen_after_bind()`を呼んで待ち受けを開始します。`listen()`のように「bindとlistenをまとめて行う」ことはできないので、bindとlistenが分かれているこの2段階の書き方になります。
## テストでの活用
単体テストで「サーバーを立ててリクエストを投げる」パターンによく使います。
```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(); });
// 別スレッドでサーバーが動いている間にテストを走らせる
httplib::Client cli("127.0.0.1", port);
auto res = cli.Get("/ping");
assert(res && res->body == "pong");
svr.stop();
t.join();
```
ポート番号がテスト実行時に決まるので、複数のテストが並列実行されても衝突しません。
> **Note:** `bind_to_any_port()`は失敗すると`-1`を返します。権限エラーや利用可能ポートが無いケースなので、返り値のチェックを忘れずに。
> サーバーを止める方法はS19. グレースフルシャットダウンするを参照してください。

View File

@@ -0,0 +1,57 @@
---
title: "S18. listen_after_bindで起動順序を制御する"
order: 37
status: "draft"
---
普通は`svr.listen("0.0.0.0", 8080)`でbindとlistenをまとめて行いますが、bindした直後に何か処理を差し挟みたいときは、2つを分けて呼べます。
## bindと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は完了。まだacceptは始まっていない
drop_privileges();
signal_ready_to_parent_process();
svr.listen_after_bind(); // acceptループを開始
```
`bind_to_port()`でポートの確保までを行い、`listen_after_bind()`で実際の待ち受けを開始します。この2段階に分けることで、bindとacceptの間に処理を挟めます。
## よくある用途
**特権降格**: 1023以下のポートにbindするにはroot権限が必要です。bindだけroot権限で行って、その後に一般ユーザーに降格すれば、以降のリクエスト処理は権限が落ちた状態で走ります。
```cpp
svr.bind_to_port("0.0.0.0", 80);
drop_privileges();
svr.listen_after_bind();
```
**起動完了通知**: 親プロセスやsystemdに「準備完了」を通知してからacceptを開始できます。
**テストの同期**: テストコードで「サーバーがbindされた時点」を確実に捉えてからクライアントを動かせます。
## 戻り値のチェック
`bind_to_port()`は失敗すると`false`を返します。ポートが既に使われている場合などです。必ずチェックしてください。
```cpp
if (!svr.bind_to_port("0.0.0.0", 8080)) {
std::cerr << "port already in use" << std::endl;
return 1;
}
```
`listen_after_bind()`はサーバーが停止するまでブロックし、正常終了なら`true`を返します。
> **Note:** 空いているポートを自動で選びたいときはS17. ポートを動的に割り当てるを参照してください。こちらも内部では`bind_to_any_port()` + `listen_after_bind()`の組み合わせです。

View File

@@ -0,0 +1,57 @@
---
title: "S19. グレースフルシャットダウンする"
order: 38
status: "draft"
---
サーバーを止めるには`Server::stop()`を呼びます。処理中のリクエストがある状態でも安全に呼べるので、SIGINTやSIGTERMを受け取ったときにこれを呼べば、グレースフルなシャットダウンが実現できます。
## 基本の使い方
```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); });
// メインスレッドで入力を待つなど
std::cin.get();
svr.stop();
t.join();
```
`listen()`はブロックするので、別スレッドで動かして、メインスレッドから`stop()`を呼ぶのが典型的なパターンです。`stop()`後は`listen()`が戻ってくるので、`join()`できます。
## シグナルでシャットダウンする
SIGINTCtrl+CやSIGTERMを受け取ったときに停止させる例です。
```cpp
#include <csignal>
httplib::Server svr;
// グローバル領域に置いてシグナルハンドラからアクセス
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()`はスレッドセーフで、シグナルハンドラの中から呼んでも安全です。`listen()`がメインスレッドで動いていても、シグナルを受けたら抜けてきます。
## 処理中のリクエストの扱い
`stop()`を呼ぶと、新しい接続は受け付けなくなりますが、すでに処理中のリクエストは**最後まで実行**されます。その後、スレッドプールのワーカーが順次終了し、`listen()`から戻ってきます。これがグレースフルシャットダウンと呼ばれる理由です。
> **Warning:** `stop()`を呼んでから`listen()`が戻るまでには、処理中のリクエストが終わるのを待つ時間がかかります。タイムアウトを強制したい場合は、シャットダウン用のタイマーを別途用意するなど、アプリケーション側の工夫が必要です。

View File

@@ -0,0 +1,57 @@
---
title: "S20. Keep-Aliveを調整する"
order: 39
status: "draft"
---
`httplib::Server`はHTTP/1.1のKeep-Aliveを自動で有効にしています。クライアントから見ると接続が再利用されるので、TCPハンドシェイクのコストを毎回払わずに済みます。挙動を細かく調整したいときは、2つのセッターを使います。
## 設定できる項目
| API | デフォルト | 意味 |
| --- | --- | --- |
| `set_keep_alive_max_count` | 100 | 1本の接続で受け付けるリクエストの最大数 |
| `set_keep_alive_timeout` | 5秒 | アイドル状態の接続を閉じるまでの秒数 |
## 基本の使い方
```cpp
httplib::Server svr;
svr.set_keep_alive_max_count(20);
svr.set_keep_alive_timeout(10); // 10秒
svr.listen("0.0.0.0", 8080);
```
`set_keep_alive_timeout()``std::chrono`の期間を取るオーバーロードもあります。
```cpp
using namespace std::chrono_literals;
svr.set_keep_alive_timeout(10s);
```
## チューニングの目安
**アイドル接続が多くてリソースを食う**
タイムアウトを短めに設定すると、遊んでいる接続がすぐに切れてスレッドが解放されます。
```cpp
svr.set_keep_alive_timeout(2s);
```
**APIが集中的に呼ばれて接続再利用の効果を最大化したい**
1接続あたりのリクエスト数を増やすと、ベンチマークの結果が良くなります。
```cpp
svr.set_keep_alive_max_count(1000);
```
**とにかく接続を使い回したくない**
`set_keep_alive_max_count(1)`にすると、1リクエストごとに接続が閉じます。デバッグや互換性検証以外ではあまりおすすめしません。
## スレッドプールとの関係
Keep-Aliveでつながりっぱなしの接続は、その間ずっとワーカースレッドを1つ占有します。接続数 × 同時リクエスト数がスレッドプールのサイズを超えると、新しいリクエストが待たされます。スレッド数の調整はS21. マルチスレッド数を設定するを参照してください。
> **Note:** クライアント側の挙動はC14. 接続の再利用とKeep-Aliveの挙動を理解するを参照してください。サーバーがタイムアウトで接続を切っても、クライアントは自動で再接続します。

View File

@@ -0,0 +1,69 @@
---
title: "S21. マルチスレッド数を設定する"
order: 40
status: "draft"
---
cpp-httplibは、リクエストをスレッドプールで並行処理します。デフォルトでは`std::thread::hardware_concurrency() - 1``8`のうち大きいほうがベーススレッド数で、負荷に応じてその4倍まで動的にスケールします。スレッド数を明示的に調整したいときは、`new_task_queue`に自分でファクトリを設定します。
## スレッド数を指定する
```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);
```
ファクトリは`TaskQueue*`を返すラムダです。`ThreadPool`にベーススレッド数と最大スレッド数を渡すと、負荷に応じて間のスレッド数が自動で増減します。アイドルになったスレッドは一定時間デフォルトは3秒で終了します。
## キューの上限も指定する
キューが溜まりすぎるとメモリを食うので、キューの最大長も指定できます。
```cpp
svr.new_task_queue = [] {
return new httplib::ThreadPool(
/*base_threads=*/12,
/*max_threads=*/0, // 動的スケーリング無効
/*max_queued_requests=*/18);
};
```
`max_threads=0`にすると動的スケーリングが無効になり、固定の`base_threads`だけで処理します。`max_queued_requests`を超えるとリクエストが拒否されます。
## 独自のスレッドプールを使う
自前のスレッドプール実装を差し込むこともできます。`TaskQueue`を継承したクラスを作り、ファクトリから返します。
```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); };
```
既存のスレッドプールライブラリがあるなら、そちらに委譲できるので、プロジェクト内でスレッド管理を統一したいときに便利です。
## ビルド時の調整
コンパイル時に変更したい場合は、マクロで初期値を設定できます。
```cpp
#define CPPHTTPLIB_THREAD_POOL_COUNT 16 // ベーススレッド数
#define CPPHTTPLIB_THREAD_POOL_MAX_COUNT 128 // 最大スレッド数
#define CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT 5 // アイドル終了までの秒数
#include <httplib.h>
```
> **Note:** WebSocket接続はその生存期間中ずっと1スレッドを占有します。大量の同時WebSocket接続を扱うなら、動的スケーリングを有効にしておきましょう`ThreadPool(8, 64)`のように)。