レスポンス構築フロー - ha1t/php-gm-server GitHub Wiki

レスポンス構築フロー

このページでは、Gemini プロトコルサーバーにおけるレスポンス生成の完全なフローを説明します。StaticFileHandler からの処理結果(MIME タイプと本体)を受け取り、ResponseBuilder で Gemini プロトコル形式のレスポンスに構築し、ステータスコードと META 情報を付与した後、クライアントに送信する一連の処理を詳細に解説します。

このフローは、GeminiServer の接続ハンドラー内で実行され、リクエスト処理フロー の最終段階に位置します。

ファイルハンドラーの処理結果

StaticFileHandlerhandle() メソッドは、リクエストパスを受け取り、以下のいずれかの結果を返します。

成功時の結果構造

ファイルが見つかった場合、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 によるレスポンス構築

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

成功レスポンス(ステータス 20)

ファイルハンドラーが結果を返した場合、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-11php-gm-server/tests/ResponseBuilderTest.php:12-15

エラーレスポンス(ステータス 51, 59)

見つからないエラー(51)

ファイルハンドラーが 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-16php-gm-server/src/GeminiServer.php:74-78

不正リクエストエラー(59)

リクエストパース失敗、またはリクエスト長超過の場合、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-68php-gm-server/src/ResponseBuilder.php:19-21

Gemini プロトコル形式の詳細

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) エラー理由 なし

各ステータスの詳細

20 成功

  • 用途: リクエストが正常に処理され、ファイルが返送される場合
  • META 部分: ファイルの MIME タイプ
  • BODY: ファイルの内容
  • 実装例: success('text/gemini', '# Welcome')

Sources: php-gm-server/tests/ResponseBuilderTest.php:12-15

51 見つからない

  • 用途: リクエストパスに対応するファイルが存在しない、またはパストラバーサルで範囲外の場合
  • META 部分: 固定値 "Not found"
  • BODY: なし(空)
  • 実装例: notFound()"51 Not found\r\n"

Sources: php-gm-server/src/ResponseBuilder.php:14-16

59 不正リクエスト

  • 用途: リクエストのパース失敗、形式違反、長さ超過の場合
  • 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

MIME タイプの自動判定

StaticFileHandler は、ファイルの拡張子から MIME タイプを自動判定します。

サポート 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 ベクタ図形
pdf 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';
}

処理フロー:

  1. リクエストパス /sub/ をドキュメントルートと結合
  2. ディレクトリ判定(is_dir()
  3. ディレクトリの場合、末尾の / を削除し /index.gmi を追加
  4. 追加後のパス /sub/index.gmi で通常のファイル処理を実行

例:

リクエストパス 実際のファイル 返送内容
/ ドキュメントルート /index.gmi の内容
/sub/ /sub/ ディレクトリ /sub/index.gmi の内容

Sources: php-gm-server/src/StaticFileHandler.php:39-41php-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["接続終了"]
Loading

データフロー図:ファイルハンドラーから送信まで

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: 接続終了
Loading

エラーハンドリングの分岐

レスポンス構築フローにおけるエラーハンドリングは、3段階で行われます。

第 1 段階:リクエスト検証エラー

条件:
- リクエストバッファに \r\n がない → 続きを待つ
- バッファ長が 1024 を超える → 59 エラー
- parse() が null を返す → 59 エラー

応答:ResponseBuilder::badRequest()

Sources: php-gm-server/src/GeminiServer.php:51-68

第 2 段階:ファイルハンドリングエラー

条件:
- ファイルが存在しない
- パストラバーサル攻撃の検出
- ファイル読み込み失敗

応答:ResponseBuilder::notFound()

Sources: php-gm-server/src/GeminiServer.php:72-78

第 3 段階:接続エラー

条件:
- TLS/TCP 接続エラー
- データ送受信エラー

応答:接続終了(レスポンス不可)

Sources: php-gm-server/src/GeminiServer.php:45-47

レスポンス構築の特性

設計パターン

ResponseBuilder は静的ファサードパターンを採用しています。これにより、以下の特性が実現されます。

特性 利点
ステートレス インスタンス化が不要。各メソッド呼び出しが独立
単一責務 Gemini プロトコル形式の組立てのみに専念
テスト容易性 純粋関数としてテストできる
再利用性 異なる場所から同一ロジックを使用可能

Sources: php-gm-server/src/ResponseBuilder.php:7-23php-gm-server/tests/ResponseBuilderTest.php:10-35

Gemini プロトコル仕様への準拠

このレスポンス構築フローは、Geminiプロトコル仕様実装 ページで詳述されている Gemini Protocol 仕様に厳密に準拠しています。

  • ステータスコード: 20(成功)、51(見つからない)、59(不正リクエスト)のみ実装
  • META 部分: ステータスごとに規定されたフォーマット
  • CRLF 区切り: すべてのレスポンスヘッダーは \r\n で終了
  • MIME タイプ: RFC 6838 に従う標準的な形式

Related Pages

⚠️ **GitHub.com Fallback** ⚠️