🧅

3StepでRailsとRackに入門する

2024/09/26に公開

この記事の背景

Railsを使っていると、Rackという言葉をよく耳にしますが、説明しろと言われると「。。。」という感じなので、
Rackとは何か、Railsでどう動いているのかをいるのかを調べてみました。
僕と同じような人が、この記事でRack説明できるようになったり、ミドルウェア層でのデバッグができるようになると嬉しいです。

Step1. RackとRackミドルウェアとは?について調べる

Rackとは?

Rackは文脈によってアーキテクチャのことなのか、ライブラリのことなのかを使い分ける必要があります。
アーキテクチャとしてのRackは、サーバーとアプリケーションの接続を簡単にするためにインターフェースの定義をしたものです。
このインターフェースの定義では下記の3つを満たすことを必要としています。

  • callメソッドに応答すること
  • callメソッドはHTTPリクエストの情報が詰まった1つの引数を取ること
  • callメソッドは、ステータス、ヘッダー、ボディの三つを含んだ配列を返すこと

GemとしてのRackはこの定義に沿って、HTTPリクエストを標準化してアプリケーション層に渡し、レスポンスを返してくれるライブラリです。
Railsでも内部的にRackを採用しているので、Railsは一つのRackアプリケーションといえます。

Rackミドルウェアとは?

Rackアプリケーションをラップする形で、リクエストの前後で処理を行うためのコンポーネントです。(抽象的すぎるので、以降のStepのサンプルをご覧ください🙇)
Rackアプリケーションに、リクエスト情報を渡す前にデータを加工したり、Rackアプリケーションが返すレスポンスに手を加工したりできます。
画像の通り入れ子構造で、データが流れてイメージです。

Step2.RailsとRackがどのように関わっているか調べる

RailsアプリケーションはRackアプリケーション?

Railsガイドから、bin/rails sコマンドは、Rackの起動コマンドをラップしたもののようです。
https://railsguides.jp/rails_on_rack.html

Railsアプリケーションには、config.ruというファイルがあります。
bin/rails sを実行すると、rackupコマンドが実行されてこのファイルが読み込まれ、Rackサーバーの設定とアプリケーションの起動を行います。

試しに下記のように書き換えてサーバーを起動すると、このファイルが読み込まれていることがわかります。

config.ru
require_relative "config/environment"
puts "=== Rackアプリケーションを起動"
run Rails.application
Rails.application.load_server

runメソッドの引数には、Rackに準拠したアプリケーションを渡す必要があるため、Rails.applicationはRackアプリケーションであることがわかります。

Railsに組み込まれたRackミドルウェアの確認

railsサーバーを起動して、下記にアクセスすると、Rackの情報が表示されます。
http://localhost:3000/rails/info/properties

下記のコマンドでも、Rackミドルウェアの一覧を確認できます。
bin/rails middleware

ここで表示されるミドルウェアは、Railsアプリケーションに組み込まれているミドルウェア全てが実行順で表示されます。
つまり、リクエストが上から順に流れていきRailsアプリケーションに渡され、レスポンスを返す時には下から上に流れていきます。

use Rack::Cors
    ↓    ↑
    use ActionDispatch::HostAuthorization
    ↓    ↑
    use Rack::Sendfile
    ↓    ↑
    use ActionDispatch::Static
    ↓    ↑
    use ActionDispatch::Executor
    ↓    ↑
    use ActionDispatch::ServerTiming
    .
    .
    ↓    ↑
    run HogeApi::Application.routes

Step3.RailsアプリケーションにRackミドルウェアを追加してみる

なんとなくRackについてわかってきたところで、最後に実際にRackミドルウェアを作成してRailsアプリケーションに追加してみます。

Rackミドルウェアの作成

標準出力だけを行う二つのミドルウェアを作成して、ミドルウェアの実行順を確認します。

lib/hello_world.rb
# Rackミドルウェアは、Rackの規約に則る必要があります
# つまり、callメソッドを持ち、引数としてenvを受け取り、
# ステータス、ヘッダー、ボディの配列を返すオブジェクトである必要があります
class HelloWorld
  def initialize(app)
    # このappは、次に呼ばれるミドルウェアやアプリケーションが入っています
    @app = app
  end

  def call(env)
    puts "Hello, World! before"
    # appは次のミドルウェアが入っているので、リクエストに対する処理はここに書きます
    status, headers, body = @app.call(env)
    # 一番下の階層のアプリケーションまでたどり着いたらここに戻ってくるので、
    # レスポンスに対する処理はここに書きます
    puts "Hello, World! after"
    # 規約に則り、ステータス、ヘッダー、ボディの配列を返しています
    [status, headers, body]
  end
end
lib/byebye_world.rb
class ByebyeWorld
  def initialize(app)
    @app = app
  end

  def call(env)
    puts "Byebye, World! before"
    status, headers, body = @app.call(env)
    puts "Byebye, World! after"
    [status, headers, body]
  end
end

Rackミドルウェアを組み込む

これだけだと、Railsアプリケーションには読み込まれないので、config/application.rbに追記します。

# config/application.rb
require_relative "../lib/byebye_world"
require_relative "../lib/hello_world"

class Application < Rails::Application
  config.middleware.use HelloWorld
  config.middleware.use ByebyeWorld
end

bin/rails middlewareで確認すると、追加したミドルウェアが設定ファイルに記載した順で表示されると思います。
上記の例でいくと、リクエストを受け付ける時は、最初にHelloWorldが実行され、その後にByebyeWorldが実行されるという流れになります。
逆に、レスポンスを返す時は、ByebyeWorldが最初に実行され、その後にHelloWorldが実行されるという流れになります。

もちろん指定した位置にミドルウェアを置くこともできます。
https://railsguides.jp/rails_on_rack.html#ミドルウェアスタックを設定する

実際に動くところを確認する

実際にサーバーを起動して、出力を確認すると、組み込んだ順に処理が進んでいることがわかります。

まとめ

Rackについて調べたり、動かしてみてRailsのサーバーとしての機能をより知ることができました。
また、ミドルウェア層でのデバッグや、ログ取得ができるようになったので、今後の開発に活かせそうです。
ご指摘を大いに受け付けていますので、コメントいただけると大変嬉しいです。

参考リンク

https://thoughtbot.com/upcase/videos/rack
https://github.com/rack/rack
https://leahneukirchen.org/blog/archive/2007/02/introducing-rack.html
https://railsguides.jp/rails_on_rack.html

Discussion