読者です 読者をやめる 読者になる 読者になる

北海道苫小牧市出身の初老PGが書くブログ

永遠のプログラマを夢見る、苫小牧市出身のおじさんのちらしの裏

PSGI 1.03のMiddlewareを書いてみる

最近Middlewareを書くことが多かったのでまとめときます。

Middlewareとは

PSGI的には、Middlewareは外から見るとPSGI Applicationsですが、別のPSGI Applicationsを動かす能力を持っているものです。平たく言えば、「$app をラップして 新たな $app として振る舞うもの」と言えます。ただしPSGIでは、この"ラップの方法"は定められていません。

ただ、現実的には Plack::Builder の enable で適用できた方が楽なので、「Plack::Middlewareを継承する」か「$appを受け取って新たな$appを返すコードリファレンス」のどちらかがいいでしょう。

入力$envを参照・変更する

一番基本的なMiddlewareのパターンです。これは簡単。コードリファレンスで実装するとこんな感じ。

my $middleware = sub {
    my $app = shift;
    return sub {
        my $env = shift;
        # ... ここで$envを変更 ...
        $app->( $env );
    };
};

出力$resのStatusとHeadersを参照・変更する

$appから出力される$resには、array referenceとcode referenceの2種類があるのでちょっと複雑です。2010-2-11時点でまだ公開されていないgithub上のpsgi-specsでは、サーバは psgi.streaming をサポートすべき(SHOULD)となっていますので、これを期待してMiddlewareを書くと以下のような感じです。

my $middleware = sub {
    my $app = shift;

    return sub {
        my $env = shift;
        die 'not supported' unless $env->{'psgi.streaming'};

        # ... $envを煮るなり焼くなり ...

        return sub {
            my $respond = shift;
            my $respond_wrapper = sub {
                my $res = shift;
                # ... $res->[0]と$res->[1]をお好きなように ...

                return $respond->( $res );  # $writerが返るかも
            };

            my $res = $app->( $env );
            ref $res eq 'CODE' ? $res->( $respond_wrapper )
                               : $respond_wrapper->( $res );
        };
    };
};

また、次の response_cb を使うと、 psgi.streaming を強制せずに $res を操作可能です。

出力$resのBodyを参照・変更する

Bodyの出力にはArray、Handle、$writerの形式等があるので、実装は大変です。素直にPlack::Middleware を継承して、 response_cb の力を借りた方がいいです*1

package MyFramework::Middleware::SomeMiddleware;
use parent qw(Plack::Middleware);

sub call {
    my( $self, $env ) = @_;
    # ... $envを好きなように調理 ...

    my $res = $self->app->($env);
    $self->response_cb( $res, sub {
        my $res = shift;  # ←$res->[0]と$res->[1]は必ずある
        # ... $res->[0]と$res->[1]を参照したり変更したり ...

        # さらにBodyを変更したい場合は、フィルタさせる関数を返すとよい
        my $done = 0;
        return sub {
            my $body_chunk = shift;
            if( $done ){
                # 最後に必ずundefを返す
                return undef;
            }elsif( defined $body_chunk ){ 
                # ... $body_chunk に好きな変更をする ...
                return $body_chunk;
            }else{
                # Bodyの最後にはundefが渡ってくる 
                $done = 1;
                return $body_footer;
        };
    } );
}

ただし、response_cb のコールバックからBodyを変更するフィルタを返すと、Content-Lengthが削除されます。Bodyを変更する必要がない時は、フィルタを返さないようにしましょう。

2010-03-29 追記

response_cbのコールバック(body filter)は、最後にundefを返す必要があります。よって、以下のようなコールバックを返してはいけません。無限ループします。

## !!CAUTION!! ダメなフィルタの例。undefが永遠に返らないので無限ループする
sub { defined $_[0] ? $_[0] : 'END' }

以下、IRC(#plack@irc.perl.org)でのログ。

13:29:28 miyagawa: the callback handler in your second test is wrong
13:29:44 miyagawa: it's causing infinite loop and that's fine
13:29:56 miyagawa: the callback handler should check if it's given undef and return undef in the next call
13:30:09 miyagawa: that's how middleware filter callback is coded

*1:2011-12-09追記: 今は Plack::Util::response_cb があるので継承しなくても使えます