本連載では第一線の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の概念図
何も処理をしないシンプルな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_cb
やPlack::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)はこちら 。>
特集1
識者がたどり着いた最適解
TypeScript最新活用
リンタ、バンドラ、ランタイム、エッジ
特集2
コミッター直伝!
速習Ruby 3.2
Wasm対応、ReDoS対策、性能改善
特集3
Tailwind CSS実践入門
まず作ってから、あとで共通化する