タイムアウト
OxPHP は、低速なクライアントや暴走するリクエストから保護するために、2 つの独立したタイムアウトを適用します。ヘッダータイムアウトは、サーバーレベルで接続フェーズを保護します。PHP の実行時間は、他の SAPI とまったく同じように、PHP 自身の max_execution_time ini ディレクティブ (および set_time_limit() ランタイム関数) によって制限されます。
仕組み
各リクエストは、次のフェーズを通過します。
- 接続の受け入れ — ヘッダータイムアウトが開始します。OxPHP は、クライアントが HTTP ヘッダーの完全なセットを送信するのを待ちます。
- ヘッダーの受信 — ヘッダータイムアウトが終了します。リクエストは PHP ワーカーにディスパッチされます。
- PHP がリクエストを処理 — アプリケーションコードは、PHP 自身の
max_execution_time(SIGALRM ベース) のもとで実行されます。上限に達すると、リクエストはキャンセルされ、統一されたRequest cancelled (timeout)の致命的エラーが発生します。 - レスポンスの送信 — keep-alive 接続では、このサイクルはステップ 1 から繰り返されます。
graph TD A["TCP connect<br/>(+ TLS handshake if enabled)"] -->|HEADER_TIMEOUT_SECONDS| B["Headers received"] B -->|max_execution_time| C["Response sent"] C -->|next request, keep-alive| A
keep-alive 接続では、両方のタイムアウトが接続内の各リクエストに対して独立して適用されます。
TLS が有効な場合、ヘッダータイムアウトは、TCP 接続が受け入れられた時点ではなく、TLS ハンドシェイクが完了した後に開始します。
ヘッダータイムアウトは、クライアントがヘッダーを 1 バイトずつ送信して接続を無期限に開いたままにする slowloris 型の攻撃から保護します。
PHP の実行時間の計測は、完全に PHP に委譲されています。max_execution_time を超過すると、OxPHP の統一されたキャンセルパスは次を実行します。
- ユーザーランドのコードが原因を検出できるように、
connection_status() & PHP_CONNECTION_TIMEOUTを設定します。 - PHP-FPM とまったく同じように、すべての
register_shutdown_function()コールバックを実行します。 - HTTP
504 Gateway Timeoutを返し、エラーログにRequest cancelled (timeout)というメッセージを書き込みます。
キャンセル時のステータスコード
OxPHP は、いくつかの異なる理由でリクエストをキャンセルします。それぞれは、汎用的な 500 ではなく、実際の状況を反映したワイヤーステータスにマッピングされます。
| 原因 | ステータス | 備考 |
|---|---|---|
max_execution_time / set_time_limit() の超過 |
504 Gateway Timeout |
サーバー側の実行時間の枯渇。 |
| サーバーのグレースフルシャットダウンによってリクエストがドレインされた | 503 Service Unavailable |
Retry-After: 5 を追加し、クライアントが復旧済みまたは置き換え後のインスタンスに対して再試行するようにします。 |
| クライアントがリクエストの途中で接続を閉じた | 499 |
nginx スタイルの「Client Closed Request」。接続はすでに失われているため、このステータスはアクセスログとメトリクスにのみ現れ、ワイヤーに書き込まれることはありません。クライアント起因の中断を非 5xx として表面化させ、サーバーエラーのアラートを汚さないようにします。 |
| スーパーバイザーによってワーカーがスタックと判定された | 500 Internal Server Error |
汎用的なサーバーエラー。原因 (デッドロック、ブロックされたシステムコールなど) は不明です。 |
| ユーザーランド起因のキャンセル | 500 Internal Server Error |
ユーザーランドは、キャンセルをトリガーする前に http_response_code() で独自のステータスを設定する場合があります。その明示的なステータスは保持されます。 |
ERROR_PAGES_DIR に 500.html しか含まれていない場合は、504.html、503.html、そして (任意で) 499.html を追加し、キャンセルの原因を問わずスタイル付きのページが一貫するようにしてください。
設定
| 変数 | デフォルト | 説明 |
|---|---|---|
HEADER_TIMEOUT_SECONDS |
5 |
接続が受け入れられてからリクエストヘッダーを受信するまでの最大秒数。slowloris 攻撃から保護します。0 は特別扱いされません — hyper に 0 秒のタイムアウトとして渡され、即座に発火します。タイムアウトを無効にするには、0 を設定するのではなく、変数を未設定にしてください |
PHP の実行時間は、OxPHP の環境変数ではなく php.ini を介して設定します。
; php.ini
max_execution_time = 30あるいは、スクリプトごとに実行時に設定します。
set_time_limit(60); // 60 seconds from now
set_time_limit(0); // disable for this request推奨値
| シナリオ | ヘッダータイムアウト | max_execution_time |
|---|---|---|
| API サーバー | 5s | 30s |
| 一般的な Web 配信 | 5s | 60s |
| ファイルアップロード | 10s | 300s |
| SSE / ロングポーリング | 5s | 0 (無効化、スクリプトごとに設定) |
これらの値は、アプリケーションの特性に基づいて調整してください。SSE エンドポイントでは、max_execution_time をグローバルに無効化するのではなく、ストリーミングスクリプトの先頭で set_time_limit(0) を呼び出してください。
トラブルシューティング
クライアントが予期せず「Request cancelled (timeout)」を伴う 504 を受け取る
スクリプトが完了する前に、PHP の実行時間の上限が発火しました。
対処: 該当するスクリプトの max_execution_time を引き上げるか、実行時に set_time_limit($seconds) を呼び出して延長してください。
// at the top of a slow script
set_time_limit(300);接続を無期限に開いたままにする必要がある SSE またはストリーミングエンドポイントでは、そのスクリプトのタイマーを無効にしてください。
set_time_limit(0);ヘッダーが到着する前に接続が切断される
ヘッダータイムアウトが、高レイテンシの回線上や低速なロードバランサーの背後にいるクライアントにとって短すぎます。
対処: ヘッダータイムアウトを増やしてください。
HEADER_TIMEOUT_SECONDS=15スクリプト変更後の最初のリクエストが OPcache によってタイムアウトする
OPcache の再コンパイルは、ファイル変更後の最初のリクエストにレイテンシを追加します。これは、ファイル数の多い開発環境でより多く発生します。開発中は max_execution_time を引き上げるか、無効にしてください。
Docker の例
services:
app:
image: ghcr.io/oxphp/oxphp:0.10.0
ports:
- "8080:8080"
environment:
HEADER_TIMEOUT_SECONDS: "5"
volumes:
- ./app:/var/www/html:ro
- ./php.ini:/usr/local/etc/php/conf.d/zz-app.ini:rophp.ini の内容は次のとおりです。
max_execution_time = 30ベストプラクティス
- 本番環境で
max_execution_time = 0をグローバルに設定しないでください。無期限の接続を必要とする SSE またはロングポーリングのエンドポイントがある場合を除きます。スクリプトごとにset_time_limit(0)を使うことを推奨します。 - API サーバーではより短い上限を使ってください。 API は応答時間が予測可能です。30 秒の
max_execution_timeは、通常のトラフィックに影響を与えることなく、スタックしたリクエストを素早く捕捉します。 - レート制限と組み合わせてください。 タイムアウトは個々のリクエストを保護し、レート制限は大量のリクエストから保護します。両者を組み合わせることで、低速な攻撃と高速な攻撃の両方のパターンをカバーできます。
関連項目
- レート制限 — IP ごとのリクエストレート制限
- Server-Sent Events (SSE) — ストリーミングエンドポイントでタイムアウトを無効にする方法のガイダンス
- 設定リファレンス — 環境変数の完全なリファレンス