前回 は、Rackのプロトコルを理解するために簡単なアプリを作りました。
今回はRackの重要な概念であるRack Middlewareについて学びます。
目次
Rack Middlewareとは
はじめにややこしいことを言いますが、Rackはミドルウェア(Middleware)です。
アプリサーバーとフレームワーク間のやりとりを仲介しているため、ミドルウェアと呼ばれます。
今回学ぶのはミドルウェアとは何か、ではなくてRack Middlewareについてです。
Rackには以下2つの概念があります。
-
Rack Application
- 前回学んだ、callメソッドを持つオブジェクトのことです
- StatusCode・Headers・Bodyの3つをレスポンスとして返します
- Rack Endpointとも呼ばれます
-
Rack Middleware
- 今回学ぶものです
- Rack Middlewareはcallメソッドを持つclassである必要があります
- Rack Applicationと違い、Responseを直接返すのではなく別の処理を呼び出しデータを加工するために使います
Rack Middlewareは、渡ってきたenv情報を加工し、次のmiddlewareまたはendpointに処理を引き渡すものです。
Hello Rack Middleware
Rack Middlewareはenv情報を加工し、次に引き渡すために利用します。
Bodyに「Hello Rack Middleware」と追加するだけのRack Middlewareを作成してみます。
class App
def call(env)
[200, { "Content-Type" => "text/plain" }, ["HELLO Rack Endpoint!\n\n"]]
end
end
# Rack Middlewareは以下条件を満たす必要がある
# - classであること
# - initializeでappを受け取ること
# - callメソッドを実装し、Status/Headers/Bodyを返すこと (Rack Endpointと同じ条件)
class HelloRackMiddleware
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
fixed_body = ["Hello Rack Middleware!\n"] + body
[status, headers, fixed_body]
end
end
use HelloRackMiddleware
run App.new
Rack Middlewareをclassとして作り、useメソッドを呼び出すことでmiddlewareを追加できます。
アプリを起動し、レスポンスを見てみます。
$ curl http://localhost:9292/
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9292 (#0)
> GET / HTTP/1.1
> Host: localhost:9292
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Transfer-Encoding: chunked
<
Hello Rack Middleware!
HELLO Rack Endpoint!
* Connection #0 to host localhost left intact
* Closing connection 0
Hello Rack Middleware!
という文字列がBodyに追加されていますね。
Middlewareはいくつでも追加できます。ただし、useの順序によってMiddlewareの動作順が異なることには注意が必要です。
class App
def call(env)
[200, { "Content-Type" => "text/plain" }, ["HELLO Rack Endpoint!\n\n"]]
end
end
class HelloRackMiddleware
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
fixed_body = ["Hello Rack Middleware!\n"] + body
[status, headers, fixed_body]
end
end
class AnotherMiddleware
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
fixed_body = ["Another Middleware!\n"] + body
[status, headers, fixed_body]
end
end
use HelloRackMiddleware
use AnotherMiddleware
run App.new
レスポンスは以下のとおりです。
Hello Rack Middleware!
Another Middleware!
HELLO Rack Endpoint!
Content-Lengthを追加するMiddleware
もう少し実用的なmiddlewareを作ってみましょう。
Response HeaderにContent-Lengthを挿入するmiddlewareを作ります。
class App
def call(env)
[200, { "Content-Type" => "text/plain" }, ["HELLO WORLD!", "Hello"]]
end
end
class ContentLengthMiddleware
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
body_size = 0
body.each do |b|
body_size += b.bytesize
end
headers["Content-Length"] = body_size.to_s
[status, headers, body]
end
end
use ContentLengthMiddleware
run App.new
アクセスしてみましょう。
$ curl -v http://localhost:9292/
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9292 (#0)
> GET / HTTP/1.1
> Host: localhost:9292
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Content-Length: 17
<
* Connection #0 to host localhost left intact
HELLO WORLD!Hello* Closing connection 0
ちゃんとContent-Lengthが設定されていますね。
Rackにあらかじめ用意されているRack Middleware
Content-Lengthを設定するmiddlewareなど、よく使うと思われるMiddlewareはrack本家で実装されています。
いくつか主要なものを紹介します。
-
Rack::ContentLength
- https://github.com/rack/rack/blob/master/lib/rack/content_length.rb
- Bodyをみて、Content-Lengthヘッダーを設定します
-
Rack::Deflater
-
Rack::ETag
-
Rack::Session::Cookie
- https://github.com/rack/rack/blob/master/lib/rack/session/cookie.rb
- cookieベースのSession管理を行います
-
Rack::Reloader
- https://github.com/rack/rack/blob/master/lib/rack/reloader.rb
- リクエストが来た際に、実行中のRubyファイルが更新されていたら自動でリロードします(開発時に利用するもの)
その他のmiddlewareは https://github.com/rack/rack/tree/master/lib/rack を参照してください。
Railsで使われているRack Middleware
Railsもrackのプロトコルに従い作成されています。
Railsで実際に使われているmiddlewareを覗いてみます。
$ rails new racktest
...省略...
$ cd racktest
$ bin/rake middleware
Running via Spring preloader in process 10795
use Webpacker::DevServerProxy
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Sprockets::Rails::QuietAssets
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use WebConsole::Middleware
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run Racktest::Application.routes
いくつかrackで実装されているMiddlewareも利用しているのがわかります。
Railsで利用しているMiddlewareについては、https://guides.rubyonrails.org/rails_on_rack.html#internal-middleware-stack にて詳しく説明されています。
まとめ
- Rack Middlewareは、渡ってきたリクエスト/レスポンスを加工するために利用する
- Middlewareはcallメソッドを実装したclassである必要がある
Rackをより理解するためには
全3回にわたって、Rackの基本的な概念を簡単なアプリを作りながら学びました。
思ったよりシンプルな構造でしたね。
よりRackを理解するための参考資料を、以下に記載しておきます。
-
Rack本家のExample
- https://github.com/rack/rack/blob/master/lib/rack/lobster.rb
- アクセスするとロブスターを返します。
bundle exec ruby lib/rack/lobster
で起動できます -
Rack::Request
,Rack::Response
を使って処理をラップしているので少し複雑ですが、基本的な考え方は今回学んだとおりです -
Response#finish
は https://github.com/rack/rack/blob/master/lib/rack/response.rb#L66 のとおりで、StatusCode/Headers/Body のペアを返します - (Paste has a Pony って、なんだろう...)
-
次世代のRackやWSGIを考えてみる
- https://qiita.com/kwatch/items/67657fef43666479bb99
- Rackの問題点について言及されています。
-
RackのHTTP/2サポートについて
- https://techracho.bpsinc.jp/hachi8833/2017_07_03/42195
- https://github.com/tenderlove/the_metal/issues/5
- Rackは今回学んだ通り、リクエストに対して1つのレスポンスを返します。しかしHTTP/2だとレスポンスは複数返さないといけないので、さてどうしたものか、という点について言及されています。