2019-06-15

今さら改めてRack middleware

Understanding 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は二種類ある

  1. バケツリレーする middleware
  2. 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

参考

About

例によって個人のなんちゃらです