Rack入門 Rack Middleware編 (3/3)


前回 は、Rackのプロトコルを理解するために簡単なアプリを作りました。
今回はRackの重要な概念であるRack Middlewareについて学びます。

目次

  1. Rack入門 概念編(1/3)
  2. Rack入門 Rack Application編 (2/3)
  3. [本記事] Rack入門 Rack Middleware編 (3/3)

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本家で実装されています。
いくつか主要なものを紹介します。

その他の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を理解するための参考資料を、以下に記載しておきます。