Common LispのWebフレームワークであるCaveman 2で遊んでいる． 今日は，リクエストの前後に処理を挿入し，レスポンスヘッダをいじったりしてみるお話． ある程度Common Lispが分かりますよ，というくらいのレベル感です．僕のレベルは，マクロが書けてうれしいね，とかいうレベルです．

全てのエンドポイントで，リクエストハンドラを処理する前・後ろに一定の共通処理を行いたい，というのは実によくある話だ．例えば認証処理を行いたければリクエストハンドラに到達する前に処理を行う必要があるし，ベンチマークのための特別なヘッダを付与するためにリクエストハンドラを通った後で処理を行いたいこともある．こういった処理は頻繁に使われるが，それらがいかに実現されているかについて運用開始後に知る機会はあまりないのが現状だ．

最近Caveman 2で自分用のアプリケーションを開発していて，そこで1からレスポンスの処理などを見て処理を構築する必要に迫られたので，勉強と探検を兼ねてこのメモをのこすことにした．

登場するファイル

この記事で登場するファイルは以下の通りだ．

web.lisp 標準的なCaveman 2アプリケーションでは src/ 以下に作成されるファイル．リクエストハンドラを定義する．

config.lisp 標準的なCaveman 2アプリケーションでは src/ 以下に作成されるファイル．アプリケーションの設定を定義する．

app.lisp アプリケーションのルートディレクトリに作成されるファイル．Caveman 2アプリケーションの頂点であり，Webサーバ(Clack)はこれを読み込む．Clackアプリケーションである．

その他，Caveman 2，Ningle，Clack，lackといったフレームワーク/ライブラリを構成するファイル

リクエストハンドラの処理後に，Gitのリビジョンをヘッダに挿入する

処理前にフックするのも後にフックするのもやる事はおおかた同じなので，今回はリクエストハンドラの処理後にフックしてみる．題材として，Caveman 2アプリケーションがあるgitレポジトリの master ブランチのリビジョンを， X-Revision ヘッダとしてレスポンスに挿入する，というシナリオを考えていこう．

defroute は何をしているのか

リクエストハンドラの前後で処理を行いたいのだから，まずはリクエストハンドラを定義する defroute から流れを追いつつ，Caveman 2がWebアプリケーションをどのように表現しているか確認していこう． Caveman 2の標準構成では，エンドポイントを定義するために defroute を使うが，これはただのマクロであって，いくつかの処理をまとめているにすぎない． defroute は， *web* にルーティング定義を書き込むためのマクロだ．

そもそもCaveman 2はWebアプリケーションフレームワークとしての最小限の機能を，より小さなフレームワークである ningle に分割した構成になっている．違う言い方をすれば， ningle にDBアクセスやコンフィギュレーション，JSON処理，テンプレートエンジンなどの付加機能を加え，使いやすくしたものがCaveman 2である．したがって，基本的な構成要素は ningle のものがそのまま流用されたり，それを継承したりしている．ここでルーティング定義を書き込む *web* も，ningleの <app> クラスそのもの*1である．

Caveman 2をとりまくコンポーネントを以下に図示する．

defroute を展開して得られる処理はおおまかには以下の通りである．

以上のような流れで， defroute によって *web* にルーティング定義が書き込まれる．

ところで，実際に web.lisp で定義されている *web* は caveman2.app:<app> の直接のインスタンスではなく， *web* とともに定義された caveman2.app:<app> のサブクラス <web> のインスタンスである．これを応用して独自のフックを後程定義していくので，頭の片隅に置いておいてほしい．

Clackアプリケーションの定義

web.lisp で *web* が定義されていることがわかった．しかし実際にこのアプリケーションを動作させるには clackup コマンドを使うか， start 関数を呼び出す必要がある． start 関数は内部的に clackup app.lisp とまったく同様のことをしているから， clackup してからどのようにリクエストハンドラに処理が渡っていき，そしてレスポンスが返されるのかを考えてみたい．

Clack はCommon Lispで動作するWebサーバを抽象化する．Webサーバの実装に依存しないWebアプリケーション用の環境を用意してくれる便利なレイヤで，Rubyにおける rack ，perlにおける plack にあたるフレームワークだ．(( Clack は仕様と実装を合わせた呼び方のようで(ちょっと自信がない)，Perlの plack が PSGI という仕様と分離されているのとは対照的だ．))

そして lack は，ningleアプリケーションや，それを継承したcaveman 2アプリケーションを，より可搬な形式であるClackアプリケーションに変換するツールだ． app.lisp が呼び出しているのはこの lack で， lack:build によって *web* をもとにしたClackアプリケーションを構築している．この過程で静的ファイル配信などが設定されている． clackup コマンドはこれを読み込み，WebアプリケーションをWebサーバとともに起動することで，実際にHTTP接続を処理できるようにする．

HTTPリクエストを受信したClackは，Webサーバからの情報を正規化し，環境と呼ばれるものを作成する*2． そして環境を引数にClackアプリケーションを呼び出す((より正確には，環境 env を引数として funcall する．これもWebサーバ別に実装されている．例: https://github.com/fukamachi/clack/blob/master/src/handler/fcgi.lisp#L52))． するとここで *web* インスタンスの call が呼び出される．なぜなら lack:builder の変換作用によって，Clackアプリケーションに対する関数呼び出しは <app> クラスの call メソッドを呼び出すように変換されているからである．

解決していそうで蛇足ですが、Lackでは可搬性を考えてナイーブに関数に変換しています。他の実現方法としてfuncallable-standard-objectをメタクラスに指定すると、オブジェクトなのにfuncallできる謎の物ができます — fukamachi (@nitro_idiot) 2018年4月14日

ここでClackの仕事は一時中断し，Caveman 2アプリケーション，そしてそのスーパークラスであるningleアプリケーションへと処理が引き渡される．

<app> クラスは call メソッドの中で，ルーティング定義に基いたリクエストハンドラのディスパッチを行う *3．ディスパッチとリクエストハンドラの処理が完了したら， <app> クラスはレスポンスをClackに返却し，HTTPリクエストに対するレスポンスが完遂される．

リクエストハンドラはコンテキストを受け取る

さて， <app> クラスはディスパッチの直前にコンテキストの初期化を行う．コンテキストは，リクエスト，レスポンス，セッションの3つで構成される． defroute で定義されたリクエストハンドラからは，これらのコンテキストを *request* ， *response* ， *session* として参照することができる． リクエストに対応するレスポンスは，最終的にこれらを加工することによって完了すると考えることができる．

そして，リクエストとレスポンスの初期化は，それぞれ make-request と make-response メソッドで行われる．

フックを作成する

ここまで来てようやく，リクエストの前後に処理を挟むための準備が整った．リクエストの前後に処理を挟むには， <app> がクラスであり，継承できることを利用する． <app> クラスを継承したサブクラスに make-response メソッドを作成し，一度スーパークラスの make-response メソッドを呼び出してからヘッダを追加すればよさそうだ．

<app> クラスのサブクラスといったが，それはもう既に <web> として web.lisp で定義されている．これにメソッドを生やして次のようにする．

( defclass <web> ( <app> ) ()) ( defmethod make-response (( app <web> ) &optional status headers body ) "<app>よりも<web>に特化したmake-responesを定義する" ( let (( res ( call-next-method ))) ( setf ( getf ( response-headers res ) :X-Revision ) "hogehoge" ) res ))

これを web.lisp に定義しておいた状態で，アプリケーションを起動してみる． / に対応するハンドラは既に定義されているものとする(デフォルトでは勝手に作成されるはずだ)．

$ cd CAVEMAN2_APP_ROOT_DIR/ $ clackup app.lisp ... Hunchentoot server is going to start. Listening on localhost:5000.

Clackアプリケーションが起動したら，別のシェルからCurlを使ってヘッダを確認する．

$ curl -I localhost:5000 # -IはHEADするオプション HTTP/1.1 200 OK Date: Sun, 15 Apr 2018 04:47:48 GMT Server: Hunchentoot 1.2.37 Transfer-Encoding: chunked X-Content-Type-Options: nosniff X-Frame-Options: DENY Cache-Control: private X-Revision: hoehoge Content-Type: text/html Set-Cookie: lack.session=...; path=/; expires=Sat, 28 Jul 2136 09:33:50 GMT

実験は成功だ!これでレスポンス前後のタイミングに処理を追加できるようになった． もう各エンドポイントに処理を一々追加して回る必要はない．

余談: :after メソッドで書き換えられる？

ここは蛇足なので，ただの備忘録として見てほしい．前述のやり方はこうだ．

<web> に make-response メソッドを生やす make-response の中で call-next-method を呼び，スーパークラスにレスポンスを作成してもらう スーパークラスが作成したレスポンスをいじって返す

ここをこうできないかと考えた．

<web> に make-response :after 補助メソッドを生やす レスポンスをいじって返す

:after 補助メソッドは基本メソッドの後から呼び出されるから，この手の変更に向いているのかと思ったが，これを実現するには :after 補助メソッドが基本メソッドから res を受け取る必要があり，その方法がよくわからなかったことと， :after メソッドの返り値が基本メソッドの返り値になるのかがよくわからなかったことから諦めた．

リビジョンを取得する

さて，長い旅路の果てにヘッダを追加することができるようになった． 今度はそのヘッダの中身である， master ブランチのリビジョンを取得して，ヘッダに設定できるようにしたい．

master ブランチのリビジョンは， .git/refs/heads/master の中身を見ることで気楽に取得できる． この処理をコードに落としてみよう．

( with-open-file ( s ( merge-pathnames #P ".git/refs/heads/master" hoge.config:*application-root* )) ( read-line s nil nil ))

上掲のコードでは，まず master へのパスを構築する．幸運にも *application-root* が，Caveman2アプリケーションのsystemが定義されている( .asd ファイルの場所がある)ディレクトリを示すパスとして config.lisp に定義されているので， merge-pathnames でパスを合体させて絶対パスを生成する．( merge-pathnames は引数の順序が直感的ではないので気を付けたい)．次にそのパスをファイルとしてオープンし，1行読んで閉じる．

これを先程のヘッダー追加処理に埋め込めば，masterブランチのリビジョンを埋め込むことができるようになる．

( defmethod make-response (( app <web> ) &optional status headers body ) "<app>よりも<web>に特化したmake-responesを定義する" ( let (( res ( call-next-method )) ( master-revision ( with-open-file ( s ( merge-pathnames #P ".git/refs/heads/master" hoge.config:*application-root* )) ( read-line s nil nil ))) ( setf ( getf ( response-headers res ) :X-Revision ) master-revision ) res ))

これで先程と同じように curl -I するとgitの master ブランチのリビジョンが表示される!やった!

$ curl -I localhost:5000 HTTP/1.1 200 OK Date: Sun, 15 Apr 2018 05:10:55 GMT Server: Hunchentoot 1.2.37 Transfer-Encoding: chunked X-Content-Type-Options: nosniff X-Frame-Options: DENY Cache-Control: private X-Revision: 29aca08a9729fec7e20e18352d7aecd0572e9ca6 Content-Type: text/html Set-Cookie: lack.session=...; path=/; expires=Sat, 28 Jul 2136 10:21:46 GMT

わ〜い!すご〜い!

高速化する

賢明な人間なら気付く話だが，これではアクセスのたびにファイルにアクセスしてしまう．性能の悪化は火を見るより明らかだ．アプリケーションの起動時に一度だけ読み込み，それ以降はメモリの上から読み込めるようにする手段さえあれば・・・そう，configを使おう．

Configは標準的なCaveman 2プロジェクトであれば config.lisp に定義されている． ファイルを眺めてみるといくつかの defconfig が定義されているのがわかるはずだ． (defconfig :common) としている箇所は，直感通り他のconfigのベースとなる．ここにリビジョン情報を保管すればよさそうだ．

Configに動的に情報を追加するのは簡単だ．なぜならConfigはただの属性リストとして表現されているからだ．これを準クォート(quasiquote)して動的にリビジョンを読み込ませれば，後はその値が使われ続ける． (defconfig :common) のリストを準クォートして，その末尾に先程のリビジョン読み込みコードを埋め込んでみよう．

( defconfig :common `( :version , ( with-open-file ( s ( merge-pathnames #P ".git/refs/heads/master" *application-root* )) ( read-line s nil nil )) ) )

あとはヘッダを追加する処理で，このConfigを読むように書き換えてみる．現在有効なConfigは， config 関数で読み出せるので・・・

( defmethod make-response (( app <web> ) &optional status headers body ) ( let (( res ( call-next-method ))) ( setf ( getf ( response-headers res ) :X-Revision ) ( config :version )) res ))

リクエスト処理の速度を殺さずに，Gitのリビジョンを返せるようになった．ClackやCaveman 2の仕組みにも詳しくなれたし，いいことづくめだ!

おわりに

ヘッダを操作する話よりも，Clack/Caveman 2のコラボレーションを解き明かす作業のほうがはるかに分量において勝ってしまったが，勉強なのでよしとしたい． フレームワークの流れを追ううちに，「それほど難しいことはやっていないのだな」ということが分かり，フレームワークは覗こうと思えば覗けるものなのだという意識ができた． 機会があれば，Clackミドルウェアなどを試しに作ってみて，どのように動くかを確認してみたい．

追記(2018.04.16)