先週末からこっちうどん屋をいじって遊んでたのだけど、HTTP::EngineにはHTTP::Engine::Middlewareというミドルウェアを作る仕組みもあるので、せっかくなのでUdonMap(Rack::URLMapもどき)をミドルウェアで実装してみた。

gist: 117012 - GitHub

前回の記事のときと構成が変わってるけど気にしない方向で。あとで前の記事直しとこう前の記事も補足した。Udon::Middleware::URLMapがミドルウェアで、server-middleware.plがミドルウェア版UdonMapを使って書き直したもの。

ミドルウェア自体は簡単に作れた。use HTTP::Engine::MiddlewareするとMooseの初期化処理をしたりミドルウェア用のメソッドを生やしたりしてくれるので、HTTP::Engineをnewするときに指定したハンドラより前に実行する処理を作りたいときは、before_handleでHTTP::Engine::Requestを受けとってHTTP::Engine::Responseを返す処理を書いてやればいいだけ。 そのミドルウェアが処理すべきRequestじゃないときや、Requestを加工して次に渡したいときは、ResponseじゃなくてRequestを返してやると次のミドルウェアやハンドラに処理が渡る様子。簡単簡単。あとモジュールの最後に__MIDDLEWARE__を書くのを忘れずに。これで後処理もしてくれる。

ちょい補足。厳密に言うと、before_handleの返す値がHTTP::Engine::Responseだったときに限り、そこでbefore_handleのループが終了する。なので次のミドルウェアに処理を回したいときは、別にHTTP::Engine::Requestを返さなきゃいけないわけではない。ただ、前のミドルウェアが返した値がそのまま次のミドルウェアもしくはrequest_handlerにリクエストとして渡るので、普通はHTTP::Engine::Requestかそれと同じ振る舞いをするオブジェクトを返すものだと思う。そうしないとミドルウェアを重ねられないし。

で、その後request_handler、after_handleの処理へと続くんだけど、もしbefore_handleがあるのうちのどれかがHTTP::Engine::Responseを返してた場合はrequest_handlerは呼ばれない。でもafter_handleはエラーでも出てない限りは呼ばれるし途中で止まることもない。つまりafter_handleを持ってるミドルウェアは全部実行される。という挙動のはず。

ところで実はこれ自体はさくっと作れたんだけど、公開しようと思ってファイルの場所移してモジュールの名前変えたらいきなり動かなくなってちょっとだけハマった。最初に書いてたときはNetaKit::Middleware::URLMapって名前にしてたんだけど、UdonMap2って名前にすると何故か「before_handleだの__MIDDLEWARE__だの、そんなもんねーよ、アホか」とPerlさんに怒られる。なんじゃろ、と思いつつソース読んでるときにその理由に気付いた。HTTP::Engine::Middlewareをuseしてるやつのモジュール名の中にMiddlewareってのが含まれてないと、そのモジュールをミドルウェアとして初期化してくれない。

# HTTP/Engine/Middleware.pmから抜粋
sub import {
    my($class, ) = @_;
    my $caller = caller;

    return unless $caller =~ /(?:\:)?Middleware\:\:.+/;

    strict->import;
    warnings->import;

    init_class($caller);

    if (Any::Moose::moose_is_preferred()) {
        Moose->import({ into_level => 1 });
    } else {
        Mouse->export_to_level( 1 );
    }

    no strict 'refs';
    *{"$caller\::__MIDDLEWARE__"} = sub {
        use strict;
        my $caller = caller(0);
        __MIDDLEWARE__($caller);
    };

    *{"$caller\::before_handle"}     = sub (&) { goto \&before_handle     };
    *{"$caller\::after_handle"}      = sub (&) { goto \&after_handle      };
    *{"$caller\::middleware_method"} = sub     { goto \&middleware_method };
}

なんでかなーとちょっと考えたんだけど、ミドルウェア作るときも使うときもuse HTTP::Engine::Middlewareなんだけど、importの中でいろいろと前処理してるので、何もしなければ使う側のときでもMooseの初期化したりbefore_handleを生やしたりしてしまう。だから多分モジュール名に制約を付けておけばよしなにやってくれるようにしてるんだろう。とか思ってたらちゃんとそう言ってるじゃん。気付けよ俺…。

というわけでHTTP::EngineとHTTP::Engine::Middlewareの使い方がなんとなく分かってきたので、そろそろうどん屋と戯れるのは終わりにして、一通りWAFっぽい動きをするものでも作ってみようかな。