Apache の sub request と internal redirect

極私的メモ。

通常のリクエスト処理フロー

modules/http/http_core.cap_process_http_connection() 関数 を ap_HOOK_process_connection として登録しており,これが(ap_HOOK_process_connection をオーバーライドしていなければ)通常のリクエスト処理の関数となります。

ap_process_http_connection() の実装について フックから Apache の全体像を追う - daily dayflower の再掲になりますが,

ap_process_http_connection()
{
    /*
     * …… request_rec の準備 ……
     */

    ap_RUN_create_request();        // RUN_ALL

    ap_RUN_post_read_request();     // RUN_ALL

    /* quick_handler が用意されてるならとっとと実行→ fast exit */
    ap_RUN_quick_handler();         // RUN_FIRST

    /* request_rec の整備(r->filename トカ,AAA トカ) */
    ap_process_request_internal();

    /* レスポンスハンドラの呼び出し */
    ap_invoke_handler();

    /* ログ吐き(どんなリクエストでも必ず実行) */
    ap_RUN_log_transaction();       // RUN_ALL
}

おおまかに

  1. request_rec の準備
  2. request_rec の整備 (via ap_process_request_internal())
  3. レスポンスハンドラの実行
  4. ファイナライズ

のような行程を経ることになります。

ちなみに ap_invoke_handler() は下記のような実装です。

ap_invoke_handler()
{
    ap_RUN_insert_filter();         // VOID

    ap_RUN_handler();               // RUN_FIRST
}

(今後頻出する)ap_process_request_internal() は下記のような実装です。

ap_process_request_internal()
{
    ap_RUN_translate_name();        // RUN_FIRST
    ap_RUN_map_to_storage();        // RUN_FIRST
    ap_RUN_header_parser();         // RUN_ALL

    ap_RUN_access_checker();        // RUN_ALL
    ap_RUN_check_user_id();         // RUN_FIRST
    ap_RUN_auth_checker();          // RUN_FIRST

    ap_RUN_type_checker();          // RUN_FIRST
    ap_RUN_fixups();                // RUN_ALL
}

レスポンスを返すことはしませんが,それ以外の request_rec にまつわる諸行(r->filename を決定したりアクセス認可をしたり)をおこなうフェーズです。

サブリクエス

サブリクエストの発行については下記の2つのステップを「基本的に」経ることになります。

  1. サブリクエストの作成(lookup)
  2. サブリクエストの実行(via ap_run_sub_req()

サブリクエストの生成

サブリクエストを生成する関数には ap_sub_req_lookup_uri(), ap_sub_req_lookup_file(), ap_sub_req_lookup_dirent(), ap_sub_req_method_uri() の4種がありますが,基本的な構造はどれも一緒です。

共通部分を抜き出すと下記のようなロジックになっています。

request_rec * ap_sub_req_lookup_hogehoge(r, next_filter)
{
    request_rec *rnew;

    rnew = make_sub_request(r, next_filter);

    ap_parse_uri(rnew, new_uri);

    if (ap_is_recursion_limit_exceeded(r)) {
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    ap_process_request_internal(rnew);

    return rnew;
}

つまり,

  1. make_sub_request() でサブリクエスト用 request_rec 構造体を初期化する
  2. ap_parse_uri()URI のパースをおこなう
  3. ap_is_recursion_limit_exceeded() でサブリクエスト・内部リダイレクト回数が所定よりオーバーしていないか検査する
  4. ap_process_request_internal()request_rec の整備をおこなう

というフローになっています。

なお ap_sub_req_method_uri()ap_sub_req_lookup_uri() の場合,ap_process_request_internal() する前に ap_RUN_quick_handler() してますが,まぁあまり気にしなくてよいでしょう。

make_sub_request() の実装

サブリクエスト用 request_rec 構造体を生成する make_sub_request() in server/request.c の実装(静的関数ですが)は下記のようになっています。

static request_rec *make_sub_request(r, next_filter)
{
    request_rec *rnew;

    /* サブリクエスト用サブプールの作成 */
    apr_pool_create(&rrp, r->pool);

    rnew = apr_pcalloc(rrp, sizeof(request_rec));

    /*
     * …… rnew の整備 ……
     */

    ap_set_sub_req_protocol(rnew, r);

    ap_RUN_create_request(rnew);

    return rnew;
}

サブメモリプールを作成しているところがポイントで,あとは request_rec 構造体を用意して初期化しているだけです。また ap_HOOK_create_request が呼び出されますので,メインリクエストであろうとサブリクエストであろうとリクエスト固有の情報の初期化に使うことができます。

上記で呼び出している ap_set_sub_req_protocol() に限っては server/protocol.c に実装があります。

ap_set_sub_req_protocol(request_rec *rnew, const request_rec *r)
{
    /*
     * …… rnew の雑多な環境整備 ……
     */

    rnew->main = (request_rec *) r;
}

重要なところはとくにないのですが,request_recmain フィールドに生成元の request_rec を設定しているところがちょっとポイントです。

サブリクエストの実行

サブリクエストを実行する ap_run_sub_req() の実装は下記のようになります。

int ap_run_sub_req(r)
{
    if (!(r->filename && r->finfo.filetype)) {
        ap_run_quick_handler(r, 0);
    }

    if (! OK) {
        ap_invoke_handler(r);
    }

    ap_finalize_sub_req_protocol(r);
}

こちらがわで response handler を実行していることがわかります。


以上をまとめると

  • サブリクエストの生成……リクエスト構造体の整備フェーズの hook が呼び出される
  • サブリクエストの実行……レスポンスハンドラが呼び出される

ということになります。

つまり,サブリクエストの出力内容(=レスポンスハンドラの役割)が必要ないのであれば,サブリクエストの生成だけおこなっても問題はありません。じっさい添付モジュールでも,サブリクエストの lookup のみおこない,その結果(r->filename 等)をあれこれするというロジックがいくつかみうけられます。これが一番最初に「2つのステップを『基本的に』経る」と書いた理由です。


さらにまとめると,

  • レスポンスに他のリクエストの処理結果を挿入したい場合,サブリクエストの lookup と ap_sub_req_run() を組み合わせて用いる
  • 他の URI のリソースの情報(実体ファイルやアクセス権など)を知りたい場合,サブリクエストの lookup のみおこなう
  • 他のリソースの取得を試みて成功した場合にそのリソースを返す場合,サブリクエストの lookup をおこなってその結果に ap_internal_fast_redirect() をおこなう(後述します)

のような適用場面があるということになります。

内部リダイレクトについて

サブリクエストの lookup には数多くの API が存在しましたが,内部リダイレクトに用いる API は基本的に ap_internal_redirect()ap_internal_redirect_handler() の2つのみです。それぞれの実装をみていきます。

まず ap_internal_redirect()

ap_internal_redirect(new_uri, r)
{
    request_rec *new = internal_internal_redirect(new_uri, r);

    ap_RUN_quick_handler(new, 0);  /* Not a look-up request */
    if (DECLINED) {
        ap_process_request_internal(new);
        if (OK) {
            ap_invoke_handler(new);
        }
    }

    if (OK) {
        ap_finalize_request_protocol(new);
    }
    else {
        ap_die();
    }
}

のようになっています。

ap_intenal_redirect_handler()

ap_internal_redirect_handler(new_uri, r)
{
    request_rec *new = internal_internal_redirect(new_uri, r);

    if (r->handler)
        ap_set_content_type(new, r->content_type);

    ap_process_request_internal(new);
    if (OK) {
        ap_invoke_handler(new);
        if (! OK) {
            ap_die();
            return;
        }

        ap_finalize_request_protocol(new);
    }
    else {
        ap_die();
    }
}

のようになっています。

細部に違いはあるものの,大まかなフローとしては,

  1. internal_internal_redirect() でリダイレクト用 request_rec の用意
  2. ap_process_request_internal()request_rec の整備
  3. ap_invoke_handler() でレスポンスハンドラの呼び出し

のようになっています。

サブリクエストと異なり,これらの関数でレスポンスハンドラの呼び出しまでおこなっています。

internal_internal_redirect() の実装

両者で使われている,内部リダイレクト用 request_rec 構造体の生成用関数 internal_internal_redirect() in modules/http/http_request.c の実装(静的関数ですが)は下記のようになります。

static request_rec *internal_internal_redirect(new_uri, r)
{
    request_rec *new;

    if (ap_is_recursion_limit_exceeded(r)) {
        ap_die(HTTP_INTERNAL_SERVER_ERROR, r);
        return NULL;
    }

    new = (request_rec *) apr_pcalloc(r->pool, sizeof(request_rec));

    /*
     * …… new の整備 (1) ……
     */

    new->prev = r;
    r->next   = new;

    ap_RUN_create_request(new);

    /*
     * …… new の整備 (2) ……
     */

    ap_RUN_post_read_request(new);

    return new;
}

フローは異なりますが,make_sub_request() と似たような感じです。ただ,ap_is_recursion_limit_exceeded() をこの関数サイドで呼んでいるところと,サブリクエストまわりでは呼ばれていない*1 ap_HOOK_post_read_request フックを呼び出しているところが異なります。

また,

    new->prev = r;
    r->next   = new;

のように,内部リダイレクト用 request_rec 構造体の prev フィールドに,呼び出し元の request_rec を設定し,呼び出し元の next フィールドに内部リダイレクト用 request_rec を設定しています。


ちなみに本筋と関係ないですが,response handler 以前の hook で内部リダイレクト処理をおこなうとおかしな挙動(クライアントからみてレスポンスが終了しない)を示します。これは Apache 1.3 API notes - Apache HTTP Server Version 2.2 にも記述されていますが,実際にわたしも経験しました*2。このように response handler 以前に内部リダイレクトをおこないたい場合,request_rec のどこか(notes とか)にリダイレクト先を保存しておいて,それ用のカスタムレスポンスハンドラを登録しておく,というのが定石になります。mod_rewrite でもそのようになっていました。

おまけ: ap_internal_fast_redirect() について

internal redirect 系で ap_internal_fast_redirect() というのもあるのですが,こいつの実装はこんな感じです。

ap_internal_fast_redirect(rr, r)
{
    /* memory pool の統合 */
    apr_pool_join(r->pool, rr->pool);

    /* サブリクエスト構造体をメインリクエスト構造体にコピー */
    copy rr => r;
}

ここからとくにコピーされたリクエストをどうこう,というのはしていません。なので response handler hook で仕掛けるにはよろしくない感じ。(ap_process_request_internal() で呼び出される)request_rec 整備系 hook から呼び出すべきでしょう。

添付モジュールでの使われどころは,たとえば mod_negotiation で,各言語用のリソースに対して sub request の lookup を行い,(ap_run_sub_req() は行わず)適切なリソースがあったらそいつに ap_internal_fast_redirect() するなど。また mod_dir で,DirectoryIndex の各ファイルに対して sub request の lookup を行い以下略とか。

つまり

  1. sub request を発行する
  2. その sub request がよさげだなと思ったら ap_internal_fast_redirect() でそれをメインリクエストに昇格
  3. あとは通常のリクエスト処理にゆだねる

な手順で使われています。

request_recnext, prev, main

いままでちらほらとでてきましたが request_rec 構造体には next, prev, main というフィールドが存在します。

struct request_rec {
    /* ...... snip snip snip ...... */

    /** Pointer to the redirected request if this is an external redirect */
    request_rec *next;
    /** Pointer to the previous request if this is an internal redirect */
    request_rec *prev;

    /** Pointer to the main request if this is a sub-request
     * (see http_request.h) */
    request_rec *main;

    /* ...... snip snip snip ...... */
};

サブリクエストに関する部分を図示すると以下のようになります。

root じゃなくて main でした。暇ができたら図を直します。

内部リダイレクトに関する部分は以下になります。

わざわざ図示するほどのことでもなかったですが,

  • 自分がサブリクエストとして呼ばれている場合 r->mainNULL 以外になる
  • 自分が内部リダイレクト先として呼ばれている場合 r->prevNULL 以外になる
  • おのおの親の親(や兄弟の兄弟)のように順々にたどっていける
    • これを用いてリクエストのループが発生しているかどうか検出可能

ことがわかります。

*1:どこかで呼んでますかね?きちんとフローを追っていないので見逃しがあるかもしれません。

*2:原因についてはきちんと読み込んでいないのでわかりませんでした。