Perl Hackers Hub

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

本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーはbayashiこと岡林大さんで、テーマは「Plack::Middleware再入門」です。

本稿のサンプルコードは、本誌サポートサイトから入手できます。

今も熱いPlack::Middleware

PSGIは「Perl Web Server Gateway Interface」の略で、Webサーバとアプリケーションをつなぐ仕様です。PlackはPSGI仕様に沿って実装されたモジュール群です。PSGI/Plackの登場によって、WebサーバとWebアプリケーションフレームワークのインタフェースが共通化されました。ChangesによるとPSGI/Plackのファーストリリースは2009年10月なので、丸7年が経過しています。Plack 1.0000からでも4年です。特殊な事情がない限り、近年Perlで実装されたWebアプリケーションはPlackに対応していると思います。

PSGI/Plackの中で、現在も新しいモジュールが登場しているクラスがあります。それは、Webサーバとアプリケーションをつなぐ層で活躍するPlack::Middlewareです。CPANには200を超えるPlack::Middlewareがアップロードされています。筆者は最近デバッグ用のPlack::Middlewareをよく書いていますが、マイクロサービス全盛の昨今、共通の認証処理などを実装している人も多いのではないでしょうか。

今回は、そのPlack::Middlewareに焦点を当てます。

Plack::Middlewareとは

Plack::Middlewareは、Webサーバとアプリケーションの間でアプリケーションに依存せずリクエストやレスポンスを処理します。ポイントはアプリケーションに依存しないという点で、たとえるならApacheやnginxのモジュールのような処理レイヤです。図1のようにアプリケーションをラップして処理を行います。

図1 Plack::Middlewareの概念図
図1 Plack::Middlewareの概念図

何も処理をしないシンプルなPlack::Middlewareの実装例は、次のようになります。

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

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

    # 1. ミドルウェアの処理

    my $res = $self->app->($env); # アプリケーションの処理

    # 2. ミドルウェアの処理

    return $res;
}

リクエストごとに呼ばれるcallメソッドで$envを受け取り、$res(PSGIレスポンス)を返します。$self->appがラップされるアプリケーションの処理で、その前後がミドルウェア本体の処理になります。

つまり、Plack::Middlewareには、

  • アプリケーションの処理の前に仕事をするもの
  • アプリケーションの処理のあとに仕事をするもの
  • アプリケーションの処理の前後両方で仕事をするもの

という3パターンがあります。

以降ではこの3パターンについて、具体的なPlack::Middlewareのモジュール実装を見ながら理解を深めます。

PSGIアプリケーションの入り口で処理

まずは、Plack::Middleware::ReverseProxyです。このミドルウェアはPSGIアプリケーションの入り口で処理をするPlack::Middlewareの代表例で、PSGIアプリケーションの前段にApacheやnginxなどのリバースプロキシがいる場合にREMOTE_ADDR(アクセス元IPアドレス)HTTP_HOST(ホストヘッダの値⁠⁠、SERVER_PORT(ポート番号)などをオリジナルのものに書き戻してくれます。

ソースコードは次のようになっています[1]⁠。

sub call {
    my $self = shift;
    my $env = shift;

    $env->{HTTPS} = $env->{'HTTP_X_FORWARDED_HTTPS'}
        if $env->{'HTTP_X_FORWARDED_HTTPS'};
    $env->{HTTPS} = 'ON'
        if $env->{'HTTP_X_FORWARDED_PROTO'}
        && $env->{'HTTP_X_FORWARDED_PROTO'} eq 'https';
    $env->{'psgi.url_scheme'} = 'https'
        if $env->{HTTPS} && uc $env->{HTTPS} eq 'ON';

    (省略)

    $self->app->($env);
}

Plack::Middleware::ReverseProxyによってアプリケーションではリバースプロキシによる環境変数の書き換えを意識せずにコーディングできます。もしこのミドルウェアがなければ、前段にいるリバースプロキシが書き換えた環境変数をアプリケーションで直接参照しなければならなくなり、ポータビリティが失われます。

このように、アプリケーションの前段で済ませておきたい処理はPlack::Middlewareが担う仕事の一つです。

PSGIアプリケーションの出口で処理

続いては、アプリケーションの処理のあとに仕事をするPlack::Middleware::SimpleContentFilterです。このミドルウェアはレスポンスをフィルタできます。

次のコードでは、$self->appのあとにContent-Typeを確認しつつ、フィルタ処理を発行しています。

sub call {
    my $self = shift;

    my $res = $self->app->(@_);
    $self->response_cb($res, sub {
        my $res = shift;
        my $h = Plack::Util::headers($res->[1]);
        return unless $h->get('Content-Type');
        if ($h->get('Content-Type') =~ m!^text/!) {
            return sub {
                my $chunk = shift;
                return unless defined $chunk;
                local $_ = $chunk;
                $self->filter->();
                return $_;
            };
        }
    });
}

$self->response_cbPlack::Utilに属する関数が登場していますが、詳細は後述します。ここでは$self->appのあとに処理をしているという構造に注目しておきましょう。

PSGIアプリケーションの入り口と出口で処理

さらに、アプリケーションの入り口と出口の両方で処理を行うPlack::Middlewareも見てみましょう。アプリケーションの実行時間をHTTPヘッダにセットしてくれるPlack::Middleware::Runtimeです。

次のコードでは、$self->appの前に時間を取っておき、レスポンスの際に経過時間をHTTPヘッダに追加するという処理を行っています。

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

    my $start = [ Time::HiRes::gettimeofday ];
    my $res = $self->app->($env);
    my $header = $self->header_name || 'X-Runtime';

    $self->response_cb($res, sub {
        my $res = shift;
        my $req_time
            = sprintf '%.6f',
                Time::HiRes::tv_interval($start);
        Plack::Util::header_set(
            $res->[1],
            $header,
            $req_time
        );
    });
}

このような処理は、$self->appをラップしているという設計であればこそです。もしアプリケーションの前後に処理が分離した設計だったら、グローバル変数で時間をやりくりするようなことになり、あまりきれいとは言えなくなっていたでしょう。

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

WEB+DB PRESS

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

2023年2月24日発売
B5判/176ページ
定価1,628円
(本体1,480円+税10%)
ISBN978-4-297-13370-2

  • 特集1
    識者がたどり着いた最適解
    TypeScript最新活用
    リンタ、バンドラ、ランタイム、エッジ
  • 特集2
    コミッター直伝!
    速習Ruby 3.2
    Wasm対応、ReDoS対策、性能改善
  • 特集3
    Tailwind CSS実践入門
    まず作ってから、あとで共通化する

おすすめ記事

記事・ニュース一覧