レスポンス構築フロー - ha1t/php-gm-server GitHub Wiki
このページでは、Gemini プロトコルサーバーにおけるレスポンス生成の完全なフローを説明します。StaticFileHandler からの処理結果(MIME タイプと本体)を受け取り、ResponseBuilder で Gemini プロトコル形式のレスポンスに構築し、ステータスコードと META 情報を付与した後、クライアントに送信する一連の処理を詳細に解説します。
このフローは、GeminiServer の接続ハンドラー内で実行され、リクエスト処理フロー の最終段階に位置します。
StaticFileHandler の handle() メソッドは、リクエストパスを受け取り、以下のいずれかの結果を返します。
ファイルが見つかった場合、StaticFileHandler は連想配列を返します。
[
'mime' => string, // MIME タイプ(例:text/gemini、image/png)
'body' => string // ファイルの内容
]Sources: php-gm-server/src/StaticFileHandler.php:33-68
以下のいずれかの場合、handle() は null を返します。
| 失敗条件 | 説明 |
|---|---|
| ファイルが見つからない | realpath() が false を返す場合 |
| パストラバーサル攻撃 | realpath() の結果がドキュメントルート外の場合 |
| ファイルではない | is_file() で false の場合 |
| 読み込み失敗 | file_get_contents() で false の場合 |
Sources: php-gm-server/src/StaticFileHandler.php:43-54
ResponseBuilder クラスは、Gemini プロトコル形式のレスポンスを構築する責務を負う静的ファサードです。リクエスト処理の結果に基づいて、3種類のレスポンスメソッドを提供します。
class ResponseBuilder
{
public static function success(string $mime, string $body): string
public static function notFound(): string
public static function badRequest(string $reason = 'Bad request'): string
}Sources: php-gm-server/src/ResponseBuilder.php:7-23
ファイルハンドラーが結果を返した場合、ResponseBuilder::success() でレスポンスを構築します。
$result = $this->fileHandler->handle($request['path']);
if ($result !== null) {
$response = ResponseBuilder::success($result['mime'], $result['body']);
}生成されるレスポンス形式:
20 {MIME}\r\n
{BODY}
例:
20 text/gemini\r\n
# Welcome to Gemini
Sources: php-gm-server/src/ResponseBuilder.php:9-11、php-gm-server/tests/ResponseBuilderTest.php:12-15
ファイルハンドラーが null を返した場合、ResponseBuilder::notFound() で 51 エラーレスポンスを構築します。
$result = $this->fileHandler->handle($request['path']);
if ($result === null) {
$response = ResponseBuilder::notFound();
// "51 Not found\r\n"
}Sources: php-gm-server/src/ResponseBuilder.php:14-16、php-gm-server/src/GeminiServer.php:74-78
リクエストパース失敗、またはリクエスト長超過の場合、ResponseBuilder::badRequest() で 59 エラーレスポンスを構築します。
// リクエスト長超過時
if (strlen($buffer) > 1024) {
$conn->write(ResponseBuilder::badRequest('Request too long'));
}
// パース失敗時
$request = RequestParser::parse($buffer);
if ($request === null) {
$conn->write(ResponseBuilder::badRequest('Invalid request'));
}Sources: php-gm-server/src/GeminiServer.php:55-68、php-gm-server/src/ResponseBuilder.php:19-21
Gemini プロトコルのレスポンスは、以下の厳密な形式に従います。
<STATUS> <META>\r\n
<BODY>
| 構成要素 | 説明 | 例 |
|---|---|---|
| STATUS | 3 桁の数字ステータスコード | 20, 51, 59 |
| SPACE | 単一のスペース(半角空白 U+0020) | |
| META | ステータスに応じた情報(最大 1024 バイト) | text/gemini、Not found |
| \r\n | CRLF(キャリッジリターン + ラインフィード) | CR+LF |
| BODY | レスポンス本体(MIME タイプに応じて) | ファイル内容 |
Sources: php-gm-server/src/ResponseBuilder.php:9-21
| ステータス | 定数 | 用途 | META 例 | BODY 有無 |
|---|---|---|---|---|
| 20 | - | 成功(SUCCESS) | MIME タイプ | あり |
| 51 | - | 見つからない(NOT FOUND) | Not found | なし |
| 59 | - | 不正リクエスト(BAD REQUEST) | エラー理由 | なし |
- 用途: リクエストが正常に処理され、ファイルが返送される場合
- META 部分: ファイルの MIME タイプ
- BODY: ファイルの内容
-
実装例:
success('text/gemini', '# Welcome')
Sources: php-gm-server/tests/ResponseBuilderTest.php:12-15
- 用途: リクエストパスに対応するファイルが存在しない、またはパストラバーサルで範囲外の場合
- META 部分: 固定値 "Not found"
- BODY: なし(空)
-
実装例:
notFound()→"51 Not found\r\n"
Sources: php-gm-server/src/ResponseBuilder.php:14-16
- 用途: リクエストのパース失敗、形式違反、長さ超過の場合
- META 部分: エラー理由("Request too long"、"Invalid request" 等)
- BODY: なし(空)
-
実装例:
badRequest('Request too long')→"59 Request too long\r\n"
Sources: php-gm-server/src/GeminiServer.php:55-66
StaticFileHandler は、ファイルの拡張子から MIME タイプを自動判定します。
| 拡張子 | MIME タイプ | 用途 |
|---|---|---|
| gmi, gemini | text/gemini | Gemini テキスト(標準) |
| txt | text/plain | プレインテキスト |
| html | text/html | HTML ドキュメント |
| css | text/css | スタイルシート |
| js | text/javascript | JavaScript コード |
| json | application/json | JSON データ |
| png | image/png | PNG 画像 |
| jpg, jpeg | image/jpeg | JPEG 画像 |
| gif | image/gif | GIF 動画像 |
| svg | image/svg+xml | SVG ベクタ図形 |
| application/pdf | PDF ドキュメント | |
| (その他) | application/octet-stream | デフォルト(バイナリ) |
Sources: php-gm-server/src/StaticFileHandler.php:11-25
ディレクトリパスへのアクセスでは、自動的に index.gmi ファイルが返送されます。
if (is_dir($filePath)) {
$filePath = rtrim($filePath, '/') . '/index.gmi';
}処理フロー:
- リクエストパス
/sub/をドキュメントルートと結合 - ディレクトリ判定(
is_dir()) - ディレクトリの場合、末尾の
/を削除し/index.gmiを追加 - 追加後のパス
/sub/index.gmiで通常のファイル処理を実行
例:
| リクエストパス | 実際のファイル | 返送内容 |
|---|---|---|
/ |
ドキュメントルート |
/index.gmi の内容 |
/sub/ |
/sub/ ディレクトリ |
/sub/index.gmi の内容 |
Sources: php-gm-server/src/StaticFileHandler.php:39-41、php-gm-server/tests/StaticFileHandlerTest.php:48-61
構築されたレスポンスは、接続ハンドラーの最終段階で TCP ソケットを通じてクライアントに送信されます。
// 成功時
$conn->write(ResponseBuilder::success($result['mime'], $result['body']));
$conn->end();
// 見つからない場合
$conn->write(ResponseBuilder::notFound());
$conn->end();
// エラー時
$conn->write(ResponseBuilder::badRequest('Request too long'));
$conn->end();Sources: php-gm-server/src/GeminiServer.php:44-82
$conn->end() は React Socket の ConnectionInterface メソッドで、レスポンス送信後に接続を閉じます。これにより、以下が保証されます。
- クライアント側でレスポンスの完全性を判定(ストリーム終了)
- サーバー側のリソースをただちに解放
- 次のリクエストを処理する準備完了
Sources: php-gm-server/src/GeminiServer.php:81
graph TD
A["クライアントリクエスト受信"] --> B["RequestParser でパース"]
B -->|パース失敗| C["ResponseBuilder::badRequest<br/>59 Invalid request"]
B -->|長さ超過| D["ResponseBuilder::badRequest<br/>59 Request too long"]
B -->|成功| E["StaticFileHandler::handle"]
E -->|ファイル見つからない| F["ResponseBuilder::notFound<br/>51 Not found"]
E -->|ファイル見つかる| G["MIME タイプ自動判定"]
G --> H["ResponseBuilder::success<br/>20 MIME"]
C --> I["レスポンス送信"]
D --> I
F --> I
H --> I
I --> J["接続終了"]
sequenceDiagram
participant Client
participant Server as GeminiServer
participant Parser as RequestParser
participant Handler as StaticFileHandler
participant Builder as ResponseBuilder
Client ->> Server: リクエスト(gemini://host/path)
Server ->> Parser: parse(buffer)
alt パース成功
Parser -->> Server: {host, path}
Server ->> Handler: handle(path)
alt ファイル発見
Handler ->> Handler: realpath 検証
Handler ->> Handler: MIME 判定
Handler -->> Server: {mime, body}
Server ->> Builder: success(mime, body)
Builder -->> Server: "20 mime\r\nebody"
else ファイル未検出
Handler -->> Server: null
Server ->> Builder: notFound()
Builder -->> Server: "51 Not found\r\n"
end
else パース失敗
Parser -->> Server: null
Server ->> Builder: badRequest()
Builder -->> Server: "59 Invalid request\r\n"
end
Server ->> Client: レスポンス送信
Server ->> Server: 接続終了
レスポンス構築フローにおけるエラーハンドリングは、3段階で行われます。
条件:
- リクエストバッファに \r\n がない → 続きを待つ
- バッファ長が 1024 を超える → 59 エラー
- parse() が null を返す → 59 エラー
応答:ResponseBuilder::badRequest()
Sources: php-gm-server/src/GeminiServer.php:51-68
条件:
- ファイルが存在しない
- パストラバーサル攻撃の検出
- ファイル読み込み失敗
応答:ResponseBuilder::notFound()
Sources: php-gm-server/src/GeminiServer.php:72-78
条件:
- TLS/TCP 接続エラー
- データ送受信エラー
応答:接続終了(レスポンス不可)
Sources: php-gm-server/src/GeminiServer.php:45-47
ResponseBuilder は静的ファサードパターンを採用しています。これにより、以下の特性が実現されます。
| 特性 | 利点 |
|---|---|
| ステートレス | インスタンス化が不要。各メソッド呼び出しが独立 |
| 単一責務 | Gemini プロトコル形式の組立てのみに専念 |
| テスト容易性 | 純粋関数としてテストできる |
| 再利用性 | 異なる場所から同一ロジックを使用可能 |
Sources: php-gm-server/src/ResponseBuilder.php:7-23、php-gm-server/tests/ResponseBuilderTest.php:10-35
このレスポンス構築フローは、Geminiプロトコル仕様実装 ページで詳述されている Gemini Protocol 仕様に厳密に準拠しています。
- ステータスコード: 20(成功)、51(見つからない)、59(不正リクエスト)のみ実装
- META 部分: ステータスごとに規定されたフォーマット
-
CRLF 区切り: すべてのレスポンスヘッダーは
\r\nで終了 - MIME タイプ: RFC 6838 に従う標準的な形式
- プロジェクト概要 — PHP GM Server の全体像と目的
- Geminiプロトコル仕様実装 — Gemini Protocol 形式の完全仕様
- GeminiServerコンポーネント — レスポンス送信を統括するサーバークラス
- ResponseBuilderコンポーネント — レスポンス構築の詳細実装
- StaticFileHandlerコンポーネント — ファイル処理の詳細実装
- RequestParserコンポーネント — リクエスト解析の詳細
- リクエスト処理フロー — リクエスト受信からレスポンス前までの全フロー
- テスト戦略と実装 — ResponseBuilder のテスト例と戦略