Perl Hackers Hub

第41回Plack::Middleware再入門(2)

前回の(1)こちらから。

さまざまなPlack::Middleware

Plack::Middlewareの3パターンについて具体的なモジュールを見ながら理解を深めたところで、CPANにアップロードされているPlack::Middlewareの中から、筆者が注目しているものをピックアップして紹介したいと思います。

なお、本節で登場するenable関数は、Plack::Middlewareをロードして有効にするもので、第1引数にミドルウェアの名前、第2引数以降にオプションを取ります。

開発を捗らせるPlack::Middleware

プロダクション環境では必要ありませんが、開発環境で使うと捗るPlack::Middlewareがあります。

DebugLogging─⁠─コンソールにデバッグ情報を出力する

Plack::Middleware::DebugLoggingは、リクエストパスやHTTPヘッダといったリクエストとレスポンスに関連するパラメータをコンソールに表示してくれます。

enable "DebugLogging";

たとえば、Content-type: application/jsonでJSONをリクエストすると、デコードしたものがダンプされるので開発が捗ります。

$ curl -X POST -H "Content-type: application/json" -d '{"user":123}' http://127.0.0.1:5000/?foo=bar

上記のようなリクエストを投げると、次のようなデバッグ出力を得ることができます。

"POST" request for "/" from "127.0.0.1"
Request Headers:
.-----------------+-----------------------------------.
| Header Name     | Value                             |
+-----------------+-----------------------------------+
| Accept          | */*                               |
| Host            | 127.0.0.1:5000                    |
| Content-Length  | 12                                |
| Content-Type    | application/json                  |
'-----------------+-----------------------------------'

Query Parameters are:
.-----------------------+-----------------------------.
| Parameter             | Value                       |
+-----------------------+-----------------------------+
| foo                   | bar                         |
'-----------------------+-----------------------------'

application/json encoded body parameters are:
{
  user => 123
}

StackTrace::LinkedSource─⁠─スタックトレースからソースをブラウザで表示する

Plackの開発環境では、エラーが発生した場合、HTMLで見やすく整形されたスタックトレースが表示されます。Plack::Middleware::StackTrace::LinkedSourceは、そのスタックトレースに含まれるモジュールのパスからソースコードをブラウザで直接参照できるようにしてくれます図2⁠。

enable "StackTrace::LinkedSource",
    lib => ['/your/app/lib', @INC];
図2 StackTrace::LinkedSourceによるソースコードの表示
図2 StackTrace::LinkedSourceによるソースコードの表示

ローカルの実行環境ならソースコードを参照するのも難しくないと思いますが、デザイナーの作業環境やQAQuality Assuarance品質保証)環境といった別環境でエラーが起きた場合に、ブラウザのスタックトレースからそのままコードが追えるので便利です。

HTTPヘッダを扱うPlack::Middleware

WebアプリケーションでHTTPヘッダを扱う場面はたくさんあります。そうしたものの中には、アプリケーションでその都度扱うよりも、Plack::Middlewareで一括して面倒を見てもらったほうが楽なものがあります。

ETag─⁠─ETagヘッダを自動で付与する

Plack::Middleware::ETagは、コンテンツの変更を検知するための一意の識別子であるETagヘッダをレスポンスに付与します。

enable "ETag";

アプリケーションで静的ファイルを配信している場合は、デフォルトでinode(ファイルの管理番号⁠⁠、mtime(最終更新日時⁠⁠、size(ファイルサイズ)の情報から値を生成します。動的コンテンツの場合は、コンテンツのダイジェストハッシュを値とします。モジュールの利用者は通常それらを意識することなく、上記の1行を書くだけで対応できます。

ConditionalGET─⁠─ステータスコード304に対応する

Plack::Middleware::ConditionalGETは、リクエストにIf-None-MatchヘッダやIf-Modified-Sinceヘッダがある場合に、レスポンスのETagヘッダやLast-Modifiedヘッダと比較して同じ値の場合は、更新がない旨を表すステータスコード304(Not Modified)を自動的に返却します。ステータスコード304はコンテンツ自体の送信が不要なので、転送量を削減できます。先に紹介したPlack::Middleware::ETagとセットで利用すると便利です。

enable "ConditionalGET";

CrossOrigin─⁠─CORSに対応する

Plack::Middleware::CrossOriginは、クロスドメインでXMLHttpRequestを行いたい場合などに必要なCORSCross-Origin Resource Sharingに対応できます。

enable "CrossOrigin",
    origins => '*',
    methods => ["GET", "POST"],
    max_age => 60*60*24*30;

CORSに対応するヘッダは、一般的にApacheやnginxなどで設定することが多いかもしれませんが、たとえばリクエストパスに応じて処理をするというような込み入った条件のある場面では、Plack::Middlewareで行ったほうが簡単かもしれません。

プロダクションに常時投入したいPlack::Middleware

ここでは、プロダクションに常時投入したいPlack::Middlewareを紹介します。

次に紹介する2つのPlack::Middlewareは、プロダクション環境に素直に投入すると処理性能がいくらか劣化します。とはいえ、取得できる情報はいざというときに重要なものですので、できれば常時運用で投入しておきたいところです。常時運用で投入するための工夫としては、たとえば、特定サーバのみで有効になるようにしたり、特定プロセスでのみ有効にしたり、特定のエンドポイントでのみ有効にするといったアプローチが考えられます。サーバ構成の状況を見ながら対応するとよいでしょう。

MemoryUsage─⁠─メモリの使用量を見る

Plack::Middleware::MemoryUsageは、ロードされたモジュールごとのメモリ使用量を計測できます。

enable "MemoryUsage",
    callback => sub {
        my ($env, $res, $before, $after, $diff) = @_;
        my $worst_count = 3;
        for my $pkg (sort {
                $diff->{$b} <=> $diff->{$a}
            } keys %$diff) {
            warn sprintf("%-32s %8d = %8d - %8d[KB]\n",
                         $pkg,
                         $diff->{$pkg}/1024,
                         $after->{$pkg}/1024,
                         $before->{$pkg}/1024,
                        );
            last if --$worst_count <= 0;
        }
    };

出力内容はcallbackオプションにコードリファレンスを自分で書く必要がありますが、上記のようにSYNOPSISをそのままコピーして動かせます。出力例は次のようになります。

B::PVOP             0 =       5 -       4[KB]
B::PVIV             0 =       2 -       1[KB]
B::IV               0 =      10 -      10[KB]

IO::Socket::INET    0 =     137 -     137[KB]
Apache2::Status     0 =      18 -      18[KB]
Symbol              0 =      19 -      19[KB]

Profiler::NYTProf─⁠─プロファイリングを取得する

Plack::Middleware::Profiler::NYTProfは、PerlのデファクトスタンダードプロファイラであるところのDevel::NYTProfをPlackアプリケーションで利用するためのミドルウェアです。

enable "Profiler::NYTProf",
    enable_profile => sub { $$ % 2 == 0 },
    env_nytprof => 'start=no:addpid=0',
    enable_reporting => 0;

prefork型のアプリケーションにおいて、親プロセスのプロファイリングを取得しないで、子プロセスだけのプロファイリングを取得できます。また、プロファイルの有効無効や結果ファイル名を動的にカスタマイズすることもできます。

Plack::Middlewareの利用時のTips

さて、ここからはPlack::Middlewareを利用するときのTipsを紹介します。

Plack::Middlewareのロード

Plack::Middlewareのロード方法は2つあります。

1つ目は、Plack::Middlewareを普通のモジュールと同じようにuseしたうえで、wrapメソッドによって追加していく方法です。

use Plack::Middleware::ETag;

my $app = sub { [ 200, [], ["OK"] ] };
$app = Plack::Middleware::ETag->wrap(
    $app,
    file_etag => [qw/size/],
);

2つ目は、次のようにPlack::BuilderのDSLDomain Specific Languageドメイン特化言語)を利用したロード方法です。

use Plack::Builder;

builder {
    enable "ETag", file_etag => [qw/size/];
    sub { [ 200, [], ["OK"] ] };
};

DSLを使ったロード方法はuseを書く必要がなく、"Plack::Middleware::"の部分を省略できるなど、短く簡潔に記述できます。

また、Plack::BuilderのDSLはplackupコマンドでも直接使えるので、Plackアプリケーションをワンライナーで起動するときに便利です。

$ plackup -e 'enable "ETag"; sub { [200, [], ["OK"]] }'

新しいミドルウェアを試してみる場合などに、いきなりアプリケーションに投入するのではなく、plackupコマンドでさくっと試すというのが簡単でお勧めです。

開発環境とプロダクション環境での切り替え

開発環境とプロダクション環境で利用するPlack::Middlewareを切り替えたい場合は、次のようにPlack::Builderが提供するenable_ifを使います。

use Plack::Builder;

builder {
    enable_if {
        $ENV{PLACK_ENV} ne "production"
    } "DebugLogging";
    sub { [ 200, [], ["OK"] ] };
}

ただし、このコードには小さな落とし穴があります。enable_ifの条件の判定はランタイムであるという点です。つまり、ミドルウェアのモジュール自体はロードされていて、条件判定はリクエストごとに実行されるのです。

気になる人は、次のようにenable自体を条件判定で囲んでしまうようにすれば、ロード自体をやめることができます。

if ($ENV{PLACK_ENV} ne "production") {
    enable "DebugLogging";
}

実行条件の複雑化への対処

enable_ifの引数に渡すブロックの条件判定は、ともすると複雑になりがちです。そうした場合には、Plack::Builder::Conditionalsの利用を検討してみましょう。リモートアドレスやリクエストパス、HTTPヘッダ、ユーザーエージェントなどの条件を見て、ミドルウェアを実行するためのDSLを提供してくれます。

use Plack::Builder;
use Plack::Builder::Conditionals;

builder {
    enable match_if addr(
        ['192.168.0.0/24', '127.0.0.1']
    ), "ReverseProxy";
    $app;
};

Plack::Middlewareのロード順と実行順序

先述したとおりPlack::Middlewareには、アプリケーションの処理の前に仕事をするもの、あとに仕事をするもの、前後両方で仕事をするものという3パターンがあります。これは、複数のPlack::Middlewareを利用する際に少し注意が必要です。

次の2つのPlack::Middlewareがあるとします。1つ目はPlack::Middleware::Fooで、2つ目はPlack::Middleware::Barです。いずれも、$self->appの前後で標準出力にミドルウェア名を出力するだけです。

Plack::Middleware::Foo
package Plack::Middleware::Foo;
use parent qw( Plack::Middleware );

sub call {
    my ($self, $env) = @_;

    say "Before Foo";

    my $res = $self->app->($env);

    say "After Foo";

    return $res;
}
Plack::Middleware::Bar
package Plack::Middleware::Bar;
use parent qw( Plack::Middleware );

sub call {
    my ($self, $env) = @_;

    say "Before Bar";

    my $res = $self->app->($env);

    say "After Bar";

    return $res;
}

この2つのミドルウェアを次のように利用します。Foo➡Barの順にenableしています。

use Plack::Builder;

builder {
    enable "Foo";
    enable "Bar";
    sub { say "App"; [ 200, [], ["OK!"] ] };
};

これを実行すると、標準出力には次のように出力されます。

Before Foo
Before Bar
App
After Bar
After Foo

enableしたのはFoo➡Barの順でしたが、出力の中でAfterのほうはBar➡Fooの順になっています。これは、もしかしたら直感と違うという人がいるかもしれません。しかし、Plack::Middlewareはアプリケーションをラップする=包むという言葉どおり、先にロードされたPlack::Middlewareが後続のアプリケーション(ミドルウェア)を内包していくしくみなのです。したがって、$self->appのあとに書かれたPlack::Middlewareの処理は、ロード順と逆に行われます。複数のPlack::Middlewareを利用する場合は、この処理順に気を付けてロードする必要があります。

<続きの(3)こちら。>

WEB+DB PRESS

本誌最新号をチェック!
WEB+DB PRESS Vol.130

2022年8月24日発売
B5判/168ページ
定価1,628円
(本体1,480円+税10%)
ISBN978-4-297-13000-8

  • 特集1
    イミュータブルデータモデルで始める
    実践データモデリング

    業務の複雑さをシンプルに表現!
  • 特集2
    いまはじめるFlutter
    iOS/Android両対応アプリを開発してみよう
  • 特集3
    作って学ぶWeb3
    ブロックチェーン、スマートコントラクト、NFT

おすすめ記事

記事・ニュース一覧