# HTTPレイヤー

`@lazarv/react-server` のプロダクションHTTPサーバーは、Node.jsの `node:http`（またはプロキシなしHTTPSの場合は `node:http2`）上に構築されており、Keep-Alive管理、リクエストタイムアウト、アドミッション制御、ヘルスチェックエンドポイント、グレースフルシャットダウンの組み込みサポートを含んでいます。これらの機能は、ロードバランサー（AWS ALB/NLB、k8s Ingressなど）の背後で実行する場合に、502エラー、コネクション枯渇、デプロイ中のリクエスト消失を防ぐために重要です。

## 設定

HTTPレイヤーのすべてのオプションは、設定ファイルの `server` セクションに配置します。すべての値には、一般的なロードバランサー設定で適切に動作する安全なデフォルト値があります。

```mjs filename="react-server.config.mjs"
export default {
  server: {
    keepAliveTimeout: 65000,
    headersTimeout: 66000,
    requestTimeout: 30000,
    maxConcurrentRequests: 100,
    maxBodyBytes: 32 * 1024 * 1024,
    shutdownTimeout: 25000,
  },
};
```

| オプション | デフォルト | 説明 |
|---|---|---|
| `keepAliveTimeout` | `65000` | アイドルコネクションを開いたままにする時間（ミリ秒）。502エラーを防ぐため、ロードバランサーのアイドルタイムアウトを超える値に設定してください。AWS ALBのデフォルトは60秒なので、65秒が安全な開始点です。 |
| `headersTimeout` | `66000` | クライアントが完全なリクエストヘッダーを送信するまでの最大待機時間（ミリ秒）。`keepAliveTimeout`を超える値に設定してください。 |
| `requestTimeout` | `30000` | クライアントが完全なリクエスト（ヘッダー＋ボディ）を送信するまでの最大時間（ミリ秒）。`0`に設定すると無効になります。 |
| `maxConcurrentRequests` | `0` | サーバーが`503 Service Busy`を返すまでの最大同時リクエスト数。`0`に設定するとアドミッション制御が無効になります。 |
| `maxBodyBytes` | `0`（無効） | 生のリクエストボディに対するパース前の上限（バイト数）。WHATWG `Request`が構築される前に強制されます。ランタイムで上限を直接適用したい場合は正の値（例：`32 * 1024 * 1024`）に設定してください。 |
| `shutdownTimeout` | `25000` | `SIGTERM`/`SIGINT`を受信後、サーバーは新しいコネクションの受け入れを停止し、処理中のリクエストが完了するまでこの時間（ミリ秒）待機してから強制終了します。k8sの`terminationGracePeriodSeconds`（デフォルト30秒）より短く設定してください。 |

## ボディサイズ上限

ボディサイズ上限はデフォルトで`0`（無効）です。本番環境ではリバースプロキシ、CDN、またはプラットフォームのエッジでボディ上限を終端することが多く、その構成ではランタイムレベルの上限を二重に持たせる意味は薄いためです。プロキシなしで実行する場合（単一ホストのデプロイ、ローカル限定サービスなど）、またはアップストリーム上限と並行して多重防御として、`maxBodyBytes`を正の値に設定するとランタイム自身が上限を適用します。

上限が有効な場合、サイズ超過のリクエストボディはハンドラがリクエストを目にする前にHTTP層で拒否されます。上限処理には2つのパスがあります：

1. **宣言された`Content-Length`のチェック。** クライアントが`Content-Length`を上限より大きく宣言した場合、サーバはボディを1バイトも読まずに即座に`413 Payload Too Large`を返します。これが安価なパスです。長さを宣言する正直なクライアントはここで止まり、クリーンなレスポンスステータスを受け取ります。
2. **読み込み中のストリーミングカウンタ。** 欠落または偽装された`Content-Length`（chunked transfer、攻撃者が制御するヘッダ）を処理します。バイト数はラップする`Transform`を通過する際にカウントされ、上限超過時にはリソース使用量を制限するため即座にソケットが破棄されます。接続クローズはクライアント側ではソケットレベルのエラーとして現れ、413ステータスは返されません。これは、丁寧なステータスコードを返すためだけに攻撃者が制御するペイロードの残りを読み取らないというトレードオフです。

上限はルートやcontent-typeにかかわらず、すべてのボディを持つ`POST` / `PUT` / `PATCH` / `DELETE`に適用されます。`serverFunctions.limits.*`内のデコード単位の上限とは独立して、それより前に実行されます。これらはServer Functionデコーダ内で後から適用されます。

メモリピークは拒否されたペイロードのサイズに関係なく、ラップするストリームの`highWaterMark`（約16 KiB）で制限されます。ラッパーはバイトが流れる際に観察しますが、決してバッファリングしません。時間はHTTPサーバの`requestTimeout`（デフォルト30秒、`server.requestTimeout`で設定可能）で制限されます。

## マルチパートのパート単位上限

`server.maxBodyBytes`はワイヤ上の合計バイト数を制限しますが、合理的なボディ上限内に収まる攻撃には対応できません：

- **高カーディナリティ**：100万個の小さなフィールド × 32バイト = 約32 MiBにすぎませんが、プラットフォームのマルチパートパーサーは100万個の`FormData`エントリとフィールドごとの文字列を割り当てます。
- **長いフィールド名**：1個のフィールドに1 MiBの名前があると、ワイヤ上のバイト数は小さいですが、パーサーは1 MiBの文字列を割り当てます。
- **ファイル偽装フィールド**：`filename=`なしの大きなブロブはパーサーで文字列フィールドとして扱われ、下流の`file()`サイズポリシーをバイパスします。

`server.multipart.*`を使うと、ストリーミングパース中にパート単位の形状を制限できます。任意のサブ制限を正の値に設定すると、マルチパートリクエストはプラットフォームの`Request.formData()`ではなく`busboy`でパースされ、設定された制限がバイトの流れる中で適用されます。いずれかの制限を超えると、問題のあるパートが完全にバッファリングされる *前* にHTTP 413で拒否されます。

```mjs filename="react-server.config.mjs"
export default {
  server: {
    multipart: {
      maxFileSize: 10 * 1024 * 1024, // ファイルあたり10 MiB
      maxFieldSize: 1 * 1024 * 1024, // テキストフィールドあたり1 MiB
      maxFiles: 10,                  // リクエストあたり最大10ファイル
      maxFields: 100,                // リクエストあたり最大100テキストフィールド
      maxParts: 200,                 // 合計200パート（ファイル＋フィールド）
      maxFieldNameSize: 200,         // フィールド名あたり200バイト
    },
  },
};
```

| 制限 | 防御対象 |
|---|---|
| `maxFileSize` | 寛容なボディ上限内でも、サイズ超過のファイルアップロード |
| `maxFieldSize` | ファイル偽装フィールド、サイズ超過のテキスト値 |
| `maxFiles` | 多数の`File`ラッパーを割り当てる多ファイル送信 |
| `maxFields` | 高カーディナリティのフィールド攻撃 |
| `maxParts` | エントリ総数の上限（ファイル＋フィールド合計） |
| `maxFieldNameSize` | 長いフィールド名による文字列割り当て攻撃 |

すべてのサブ制限はデフォルトで`0`（無効）です。*すべての*サブ制限が無効の場合、busboyは呼び出されず、マルチパートボディはプラットフォームパーサーにそのまま渡されます。オーバーヘッドはゼロです。

パースされた`FormData`はプラットフォームパーサーが生成するものと機能的に同等です（filename、MIMEタイプ、サイズ、バイトが保持されます）。パートごとの`Content-Transfer-Encoding`のみ異なりますが、HTML5仕様は`multipart/form-data`に対してこれを廃止しており、モダンなブラウザは送信しないため、実用上の影響はありません。統合テストスイートのA/B同等性テストがこの特性を保証します。

この上限は本ランタイムが提供する全アダプタターゲットに適用されます。Nodeパスは受信リクエストをbusboyで直接消費し、エッジ／サーバレスパスはNodeのWeb Streams相互運用を介してWeb `Request`ボディを同じパーサに適合させます。両パスは同じパーサコアを共有するため、パートごとの上限のセマンティクスは同一です。ボディ上限（`server.maxBodyBytes`）も同様に移植可能で、宣言された`Content-Length`をヘッダーから確認した後、ボディを`maxBodyBytes + 1`まで読み取り、オーバーフローした場合は即座に413を返します。Node互換APIをサポートしないネイティブエッジランタイムでは、パートごとのマルチパート上限は静かにプラットフォームパーサにダウングレードされますが、ボディ上限は引き続き適用されます。

## CSRF / Originバリデーション

`server.csrf`はリクエストの`Origin`（または`Referer`）ヘッダを信頼済みオリジン集合と照合することで、サーバ関数のアクションPOSTをクロスサイトリクエストフォージェリ（CSRF）から防御します。

脅威の範囲は最初に思えるよりも限定的です。JS駆動のアクション呼び出し（`react-server-action`カスタムヘッダ付きの`fetch()`）はすでに安全です。任意のカスタムヘッダはリクエストをCORS非単純化するため、ブラウザはプリフライトを送信し、ランタイムは要求外のクロスオリジンプリフライトを拒否します。明示的な防御が必要なのは**フォーム送信アクションPOST**です：`multipart/form-data`ボディと`$ACTION_ID_`フィールドを持つ``。この形式はCORS単純であり、ブラウザはプリフライトなしで送信するため、受信側のアプリがソースを検証しない限り、悪意のあるサイトがクロスオリジンでフォーム送信できてしまいます。

```mjs filename="react-server.config.mjs"
export default {
  server: {
    csrf: {
      mode: "lax",                                // デフォルト
      allowedOrigins: [
        "https://host.example.com",
        /^https:\/\/[^.]+\.partner\.com$/,
      ],
    },
  },
};
```

| モード | Origin / Refererなし | Originあり & 信頼済み | Originあり & 信頼外 |
|---|---|---|---|
| `"lax"`（デフォルト） | 許可 | 許可 | **403** |
| `"strict"` | **403** | 許可 | **403** |
| `false` / `"off"` | 許可 | 許可 | 許可 |

**信頼済みオリジン集合**は既存の設定から暗黙的に構築されます：

1. リクエスト自身の解決済みオリジン（プロキシ対応）— 同一オリジンのフォーム送信は設定なしで常に動作
2. `server.origin` — 正規の設定済みアイデンティティ
3. `server.cors.origin` / `origins`（明示的な値で設定されている場合のみ、`*` / `true`は除く）— CORS信頼済みパートナーは通常CSRF信頼済みでもあります
4. `server.csrf.allowedOrigins` — CSRF信頼がCORS信頼と異なるケースのための明示的な追加

**リモートコンポーネント：明示的な設定が必要なケース。** ホストアプリがこのアプリのリモートコンポーネントを埋め込む場合、ユーザのブラウザにはリモート（このアプリ）をターゲットとするフォームが表示されます。送信時、ブラウザは`Origin: `でリモートにクロスオリジンPOSTします。`server.csrf.allowedOrigins`にエントリがなければ、リモートは正当なフォーム送信を403で拒否します。これは意図的な設計です — リモートオペレータは、どのホストオリジンがアクションエンドポイントを呼び出せるかを明示的に宣言しなければなりません。

```mjs filename="remote-app/react-server.runtime.config.mjs"
export default {
  server: {
    cors: true,
    csrf: {
      allowedOrigins: [
        "https://host.example.com",
        "https://staging-host.example.com",
      ],
    },
  },
};
```

**拒否レスポンス：** ヘッダ`x-react-server-action-error: csrf_origin_mismatch`（またはOriginなしのstrictモードでは`csrf_origin_missing`）付きのHTTP `403 Forbidden`。ハンドラは実行されず、ボディはパースされません。

**この機能の対象外：** トークンベースCSRF（double-submit cookie / セッション単位のnonce）。これは高価値アクションに適した、より厳格な防御ですが、ランタイムが代わりに合成できないセッション認識が必要です。これを必要とするアプリは、アクションディスパッチの前段ミドルウェアとして実装できます。

## Keep-Aliveとタイムアウト

Node.jsのデフォルトの `keepAliveTimeout` は5秒であり、ロードバランサーがある環境では短すぎます。ロードバランサーよりも先にサーバーがアイドルコネクションを閉じると、ロードバランサーはサーバーが既に切断したコネクションでリクエストを送信する可能性があり、**502 Bad Gateway** が発生します。

`@lazarv/react-server` のデフォルト値は、これを回避するように選択されています：

- `keepAliveTimeout`（65秒）はAWS ALBのデフォルトアイドルタイムアウト（60秒）を超えます
- `headersTimeout`（66秒）はNode.jsの要件通り `keepAliveTimeout` を超えます
- `requestTimeout`（30秒）は低速またはストールしたクライアントがソケットを無期限に保持するのを防ぎます

## アドミッション制御

`maxConcurrentRequests` が `0` より大きい値に設定されている場合、サーバーは処理中のリクエストを追跡し、制限に達すると `503 Service Busy`（`Retry-After: 1` ヘッダー付き）で応答します。これにより、すべてのリクエストがCPU/メモリを同時に奪い合い、すべてが遅くなるのではなく、一部を高速に処理し残りを拒否するサンダリングハードシナリオを防ぎます。

カウンターはレスポンスが完全に送信された後にデクリメントされるため、ストリーミングレスポンスでも正確な追跡が保証されます。エラーパスでもカウンターは適切にデクリメントされます。

## アダプティブバックプレッシャー

`@lazarv/react-server` はプロダクション環境で**デフォルトで有効**なアダプティブバックプレッシャーシステムを搭載しています。**イベントループ使用率（ELU）** — `performance.eventLoopUtilization()` — を使用してNode.jsのイベントループ飽和度を直接測定します。CPU%やレイテンシーベースのアルゴリズムとは異なり、ELUはワークロードの不均一性（高速ルートと低速ルートの切り替え）の影響を受けず、イベントループ自体が真に飽和したときのみ上昇します。

制御ループは**AIMD（加法増加・乗法減少）**を使用します：
- **ELU &lt; 0.95**: ウィンドウごとに `√limit` ずつ制限を増加（高速回復）
- **ELU ≥ 0.95**: ウィンドウごとに10%ずつ制限を減少（緩やかなバックオフ）

リミッターは全開（`initialLimit = maxLimit`）で開始し、ファストパスで**オーバーヘッドゼロ** — 通常の負荷では不可視で、イベントループが真に飽和したときのみ制限を強化します。

カスタマイズまたは無効にするには `server.backpressure` を使用します：

```mjs filename="react-server.config.mjs"
export default {
  server: {
    backpressure: {
      enabled: true,        // falseで無効化
      initialLimit: 1000,   // 開始制限（デフォルトはmaxLimit）
      minLimit: 1,          // 下限
      maxLimit: 1000,       // 上限
      eluMax: 0.95,         // ELU 95%超でキューをスキップ
      sampleWindow: 1000,   // 1秒ごとに再計算
      smoothingFactor: 0.2, // EWMAレイテンシー平滑化
      queueSize: 100,       // スロット待ちの最大リクエスト数
      queueTimeout: 5000,   // 503までの最大待機時間（ミリ秒）
    },
  },
};
```

| オプション | デフォルト | 説明 |
|---|---|---|
| `enabled` | `true` | アダプティブバックプレッシャーを有効化。`false`に設定すると無効になり、静的な`maxConcurrentRequests`にフォールバックします。 |
| `initialLimit` | `maxLimit` | 開始時の同時実行制限。デフォルトは`maxLimit`（最初は全開、過負荷時に制限）。 |
| `minLimit` | `1` | 下限 — アダプティブ制限はこの値を下回りません。 |
| `maxLimit` | `1000` | 上限 — 両方が設定されている場合、`maxConcurrentRequests`で制限されます。 |
| `eluMax` | `0.95` | 制限が縮小し、超過リクエストがキューをスキップするELUレベル（0–1）。 |
| `sampleWindow` | `1000` | 再計算とELUサンプリングの間隔（ミリ秒）。 |
| `smoothingFactor` | `0.2` | レイテンシー平滑化のEWMA係数（0–1）。高い値 = より反応的。 |
| `queueSize` | `100` | バックプレッシャーキューで待機できる最大リクエスト数。満杯の場合、追加のリクエストは即座に503で拒否されます。 |
| `queueTimeout` | `5000` | リクエストがキューで待機する最大時間（ミリ秒）。503で拒否されるまでの時間です。ロードバランサーのリクエストタイムアウトより短く設定してください。 |

`backpressure.enabled` と `maxConcurrentRequests` の両方が設定されている場合、静的制限がアダプティブ制限のハードシーリングとして機能します。これにより安全ネットが提供されます：アルゴリズムは `maxConcurrentRequests` まで探索できますが、それを超えることはありません。

### キューの仕組み

同時実行制限に達したとき、リクエストを即座に拒否するのではなく、リミッターは制限付きのFIFOキューに配置します。処理中のリクエストが完了すると、解放されたスロットは汎用プールに戻るのではなく、次のキュー待ちのリクエストに直接渡されます — 公平な順序を保証します。

リクエストは以下の場合にキューから削除されます：
- スロットが利用可能になった場合 → リクエストは通常通り処理されます
- `queueTimeout` が期限切れになった場合 → リクエストは503で拒否されます
- クライアントが切断した場合 → リクエストはサイレントに破棄されます（無駄な作業なし）
- ELUが `eluMax` を超えた場合 → リクエストはキューを完全にバイパスし、即座に拒否されます

これにより、短いトラフィックバーストは透過的に吸収されながら、持続的な過負荷時には負荷が適切にシェッドされます。

> **ヒント:** デフォルト値で開始し、監視してください。リミッターは統計情報（現在の制限、処理中の数、キュー深度、ELU、平滑化されたレイテンシー）を公開しており、これをオブザーバビリティスタックに送信してワークロードに合わせてパラメーターを調整できます。

## ヘルスチェックエンドポイント

プロダクションサーバーは、Kubernetesのlivenessプローブおよびreadinessプローブ用に2つの組み込みエンドポイントを公開しています。これらのエンドポイントはミドルウェアチェーンの最上位に登録されており、最小限のレイテンシーのために他のすべてのミドルウェアをバイパスします。

| エンドポイント | 目的 | レスポンス |
|---|---|---|
| `/__react_server_health__` | Livenessプローブ | `200 ok` — プロセスが生存中 |
| `/__react_server_ready__` | Readinessプローブ | ワーカースレッドが実行中の場合は`200 ok`、ワーカーが終了している場合は`503 not ready` |

Kubernetes Podスペックの例：

```yaml
livenessProbe:
  httpGet:
    path: /__react_server_health__
    port: 3000
  initialDelaySeconds: 5
  periodSeconds: 10
readinessProbe:
  httpGet:
    path: /__react_server_ready__
    port: 3000
  initialDelaySeconds: 3
  periodSeconds: 5
```

> **ヒント:** livenessプローブは `/` ではなく `/__react_server_health__` に向けてください。ヘルスエンドポイントはSSRパイプラインに触れることなく即座にレスポンスを返すため、レンダリング高負荷時に誤って失敗することがありません。

## グレースフルシャットダウン

サーバーが `SIGTERM` または `SIGINT` を受信した場合：

1. 新しいコネクションの受け入れを停止します
2. 処理中のリクエストは完了が許可されます
3. `shutdownTimeout` ミリ秒後にプロセスが強制終了します

[クラスタモード](/ja/features/cluster)では、プライマリプロセスはすべてのワーカーがドレインされるまで待機してから終了します。通常の運用中にワーカーが予期せず終了した場合、サービス全体を停止するのではなく、自動的に再起動されます。

これにより、Kubernetesやその他のコンテナオーケストレーターでのゼロダウンタイムローリングデプロイメントが保証されます。デフォルトの `shutdownTimeout` の25秒は、k8sのデフォルトの `terminationGracePeriodSeconds`（30秒）内に5秒のバッファーを残します。