今さら改めてRack middleware
がとても分かりやすかったのでこれをもとにしたいと思う。
Rackの基本構造
- Rack middleware はたまねぎ構造で rack app が他の rack app をくるんだ形
- 受け取った request を外側の app が内側の app へバケツリレーし、response を内側の app から外側の app へリレーする
- この組み立てのために Rack::Builder の use と run を使う
Rackアプリの基本形
- callメソッドを持っていること
だけ。
call メソッドを持っていれば class だろうが Proc オブジェクトだろうが Method オブジェクトだろうがなんでもよい。
まぁ本当は call メソッドの引数の型と戻り値の型がどうってのはあって、Hash を受け取って Rack::Response を finish して返しなさいってことなんだけど、それは置いておく。
Rack middlewareは二種類ある
- バケツリレーする middleware
- response を直接返す middleware
どちらも Lint 的には valid なので少しややこしいのと、直接 response を返してるのは middleware と呼ぶのか?という気はするけど、まぁそういうものらしい。
Rack middlewareはclassで
説明は以下に続くんだけど、とにかく
middlewareはアプリと違ってcallメソッドを持っているだけではダメで、classにする
と覚えておくこと。
前者のバケツリレーを実現するたまねぎ構造のために
rack app のところでは call があればよいという話だったんだけど、たまねぎ構造のためには app を受け取ってリレーする必要がある。これをどう実現するのかというと、ちょうどいいサンプルが rack 2.0.7 の
Rack::Builder#use
のコメントに例が書かれているので貼っておく。
# class Middleware
# def initialize(app)
# @app = app
# end
#
# def call(env)
# env["rack.some_header"] = "setting an example"
# @app.call(env)
# end
# end
- initialize で app を受け取ってどこかのインスタンス変数に保存しておく
- call で env を受け取って、call の中で保存しておいた app を取り出してそいつの call を呼ぶ
という形になっている。どこでこれが決まっているかというと、use の中のこれ。
@use << proc { |app| middleware.new(app, *args, &block) }
つまり、
use Middleware, *args, &block
と書くと
Middleware.new(app, *args, &block)
で他の app をくるむので、initialize で受け取る処理を書きましょう、となる。
直接返す
直接返すっていうか、直接ではないんだけど、要はこの @app.call(env) の実行がなければ内側の middleware, app は呼び出されないので、外側の middleware だけで処理が完結するという形。
例えば Rack::Attack という middleware があるんだけど、この 6.0.0 の call は以下のようになっている。
def call(env)
env['PATH_INFO'] = PathNormalizer.normalize_path(env['PATH_INFO'])
request = Rack::Attack::Request.new(env)
if safelisted?(request)
@app.call(env)
elsif blocklisted?(request)
self.class.blocklisted_response.call(env)
elsif throttled?(request)
self.class.throttled_response.call(env)
else
tracked?(request)
@app.call(env)
end
end
アクセスを block するのが目的なので、 blocklist に引っかかった場合は中の app は呼ばれず、自分で用意している response を返す形になっている。
順番をあとから制御したい場合はどうしたらいいの?
基本的に rack middleware は use で並べた順番にだんだんと内側に middleware をくるんでいくので、この順番で実行される。
しかし例えば Rails では
middleware.insert 0, Middleware
という書き方で順番を後から制御できる。しかしこの記法、これは Rack の機能じゃないのです。Rails::Configuration::MiddlewareStackProxy ってやつがいい具合にやってくれている。
逆にこの機能に依存せずに request を受け取ってすぐに受け取る何かを作ろうと思ったらどうするか?
そう、Rack は app を middleware でくるめばいいだけなので、config.rb で
run Middleware.new(Rails::Application)
みたいなことをやればよい。なるほどなぁ。
middlewareの中身はこんな感じ
class Middleware
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
res = Rack::Response.new(body, status, headers)
res.xxxxx # なんかやる
res.finish
end
end