HTTPレイヤー
@lazarv/react-server のプロダクションHTTPサーバーは、Node.jsの node:http(またはプロキシなしHTTPSの場合は node:http2)上に構築されており、Keep-Alive管理、リクエストタイムアウト、アドミッション制御、ヘルスチェックエンドポイント、グレースフルシャットダウンの組み込みサポートを含んでいます。これらの機能は、ロードバランサー(AWS ALB/NLB、k8s Ingressなど)の背後で実行する場合に、502エラー、コネクション枯渇、デプロイ中のリクエスト消失を防ぐために重要です。
HTTPレイヤーのすべてのオプションは、設定ファイルの server セクションに配置します。すべての値には、一般的なロードバランサー設定で適切に動作する安全なデフォルト値があります。
react-server.config.mjsexport 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つのパスがあります:
- 宣言された
Content-Lengthのチェック。 クライアントがContent-Lengthを上限より大きく宣言した場合、サーバはボディを1バイトも読まずに即座に413 Payload Too Largeを返します。これが安価なパスです。長さを宣言する正直なクライアントはここで止まり、クリーンなレスポンスステータスを受け取ります。 - 読み込み中のストリーミングカウンタ。 欠落または偽装された
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で拒否されます。
react-server.config.mjsexport 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をサポートしないネイティブエッジランタイムでは、パートごとのマルチパート上限は静かにプラットフォームパーサにダウングレードされますが、ボディ上限は引き続き適用されます。
server.csrfはリクエストのOrigin(またはReferer)ヘッダを信頼済みオリジン集合と照合することで、サーバ関数のアクションPOSTをクロスサイトリクエストフォージェリ(CSRF)から防御します。
脅威の範囲は最初に思えるよりも限定的です。JS駆動のアクション呼び出し(react-server-actionカスタムヘッダ付きのfetch())はすでに安全です。任意のカスタムヘッダはリクエストをCORS非単純化するため、ブラウザはプリフライトを送信し、ランタイムは要求外のクロスオリジンプリフライトを拒否します。明示的な防御が必要なのはフォーム送信アクションPOSTです:multipart/form-dataボディと$ACTION_ID_<token>フィールドを持つ<form method="POST">。この形式はCORS単純であり、ブラウザはプリフライトなしで送信するため、受信側のアプリがソースを検証しない限り、悪意のあるサイトがクロスオリジンでフォーム送信できてしまいます。
react-server.config.mjsexport default {
server: {
csrf: {
mode: "lax", // デフォルト
allowedOrigins: [
"https://host.example.com",
/^https:\/\/[^.]+\.partner\.com$/,
],
},
},
};
| モード | Origin / Refererなし | Originあり & 信頼済み | Originあり & 信頼外 |
|---|---|---|---|
"lax"(デフォルト) | 許可 | 許可 | 403 |
"strict" | 403 | 許可 | 403 |
false / "off" | 許可 | 許可 | 許可 |
信頼済みオリジン集合は既存の設定から暗黙的に構築されます:
- リクエスト自身の解決済みオリジン(プロキシ対応)— 同一オリジンのフォーム送信は設定なしで常に動作
server.origin— 正規の設定済みアイデンティティserver.cors.origin/origins(明示的な値で設定されている場合のみ、*/trueは除く)— CORS信頼済みパートナーは通常CSRF信頼済みでもありますserver.csrf.allowedOrigins— CSRF信頼がCORS信頼と異なるケースのための明示的な追加
リモートコンポーネント:明示的な設定が必要なケース。 ホストアプリがこのアプリのリモートコンポーネントを埋め込む場合、ユーザのブラウザにはリモート(このアプリ)をターゲットとするフォームが表示されます。送信時、ブラウザはOrigin: <host origin>でリモートにクロスオリジンPOSTします。server.csrf.allowedOriginsにエントリがなければ、リモートは正当なフォーム送信を403で拒否します。これは意図的な設計です — リモートオペレータは、どのホストオリジンがアクションエンドポイントを呼び出せるかを明示的に宣言しなければなりません。
remote-app/react-server.runtime.config.mjsexport 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)。これは高価値アクションに適した、より厳格な防御ですが、ランタイムが代わりに合成できないセッション認識が必要です。これを必要とするアプリは、アクションディスパッチの前段ミドルウェアとして実装できます。
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 < 0.95: ウィンドウごとに
√limitずつ制限を増加(高速回復) - ELU ≥ 0.95: ウィンドウごとに10%ずつ制限を減少(緩やかなバックオフ)
リミッターは全開(initialLimit = maxLimit)で開始し、ファストパスでオーバーヘッドゼロ — 通常の負荷では不可視で、イベントループが真に飽和したときのみ制限を強化します。
カスタマイズまたは無効にするには server.backpressure を使用します:
react-server.config.mjsexport 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スペックの例:
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 を受信した場合:
- 新しいコネクションの受け入れを停止します
- 処理中のリクエストは完了が許可されます
shutdownTimeoutミリ秒後にプロセスが強制終了します
クラスタモードでは、プライマリプロセスはすべてのワーカーがドレインされるまで待機してから終了します。通常の運用中にワーカーが予期せず終了した場合、サービス全体を停止するのではなく、自動的に再起動されます。
これにより、Kubernetesやその他のコンテナオーケストレーターでのゼロダウンタイムローリングデプロイメントが保証されます。デフォルトの shutdownTimeout の25秒は、k8sのデフォルトの terminationGracePeriodSeconds(30秒)内に5秒のバッファーを残します。