Rails と Rack について

この記事は、「フィヨルドブートキャンプ Part 2 Advent Calendar 2023」15日目の記事です。

フィヨルドブートキャンプ Part 2 Advent Calendar 2023 - Adventar

Part 1はこちらになります。

フィヨルドブートキャンプ Part 1 Advent Calendar 2023 - Adventar

前回は、dawaさんの「テストとは。最初のイシューでエラーに遭遇した話。」でした。

この記事は、Rails.applicationを調べた過程で学んだことをまとめたものになります。

この記事で伝えたいことは、次の2つです。

  • RailsはRackアプリケーションである
  • Rackとは「WebサーバーとRuby製Webアプリケーションがやりとりを行うための規約」

では、私がRails.applicationの存在を知ったきっかけから始めましょう。

Rails.applicationとの出会い

以前Railsコンソール上でpolymorphic_urlの挙動を調べようとしたことがあり、その時にappメソッドの存在を知りました。

Rails::ConsoleMethods

このメソッドを利用すると、次の様に名前付きルーティングヘルパーを呼び出したり、リクエストを投げることが可能です。

irb(main):001> app.books_path
=> "/books"

irb(main):002> app.polymorphic_url(Book.new)
=> "http://www.example.com/books"

irb(main):003> app.get "http://localhost:3000/books"
Started GET "/books" for 127.0.0.1 at 2023-12-10 10:53:13 +0900
  ActiveRecord::SchemaMigration Load (0.5ms)  SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
Processing by BooksController#index as HTML
  Rendering layout layouts/application.html.erb
  Rendering books/index.html.erb within layouts/application
  Book Load (0.6ms)  SELECT "books".* FROM "books"
  Rendered books/_book.html.erb (Duration: 0.3ms | Allocations: 275)
  Rendered books/_book.html.erb (Duration: 0.0ms | Allocations: 54)
  Rendered books/_book.html.erb (Duration: 0.0ms | Allocations: 53)
  Rendered books/_book.html.erb (Duration: 0.0ms | Allocations: 53)
  Rendered books/index.html.erb within layouts/application (Duration: 2.7ms | Allocations: 2243)
Everything's up-to-date. Nothing to do
  Rendered layout layouts/application.html.erb (Duration: 15.9ms | Allocations: 15124)
Completed 200 OK in 30ms (Views: 17.9ms | ActiveRecord: 0.2ms | Allocations: 19944)


=> 200

RailsAPIにはappメソッドの説明もありました。

reference the global “app” instance, created on demand. To recreate the instance, pass a non-false value as the parameter.

「グローバルな"app"インスタンスを参照する」とあります。 グローバルな"app"インスタンスってなんだ?と気になり、コードを見てみることに。

module Rails
  module ConsoleMethods
    # reference the global "app" instance, created on demand. To recreate the
    # instance, pass a non-false value as the parameter.
    def app(create = false)
      @app_integration_instance = nil if create
      @app_integration_instance ||= new_session
    end

    # create a new session. If a block is given, the new session will be yielded
    # to the block before being returned.
    def new_session
      app = Rails.application
      session = ActionDispatch::Integration::Session.new(app)

      # This makes app.url_for and app.foo_path available in the console
      session.extend(app.routes.url_helpers)
      session.extend(app.routes.mounted_helpers)

      session
    end

rails/railties/lib/rails/console/app.rb at 6b93fff8af32ef5e91f4ec3cfffb081d0553faf0 · rails/rails

appメソッド内で呼ばれているnew_sessionメソッドは、Rails.applicationを引数にしてActionDispatch::Integration::Sessionインスタンス化したものを返しています。

ActionDispatch::Integration::Sessionインスタンスは、テストプロセスによって順次実行されるリクエストとレスポンスのセットを表す、とのこと。

Rails.applicationに関しては、Railsガイドに説明がありました。

Rails.applicationは、Railsアプリケーションにおける主要なRackアプリケーションです。Rackに準拠したWebサーバーで、Railsアプリケーションを提供するには、Rails.applicationオブジェクトを使う必要があります。

Rails と Rack - Railsガイド

Rack って聞いたことはあるけど何なんだろう?と気になったので、Rack について調べてみることにしました。

Rackとは?

Rackについて簡単にまとめると、次の様になります。

「Rackとは、WebサーバーとRuby製Webアプリケーションがやりとりを行うための規約」のこと。

1PumaやUnicornといったWebサーバーとRailsアプリケーションがやりとりを行えるのは、RailsがRackの規約に沿って実装されているからなのです。

WebサーバーとRailsアプリケーションのやり取りの中身は、Rackアプリケーションの仕様から知ることができます。

Rackアプリケーションとは、以下の条件を満たすRubyオブジェクトのことを言います。

  • environmentという引数一つを受け取る、callメソッドを呼び出すことができる。
  • status headers bodyという三つの値を含む、凍結されていない配列を返す

rack/SPEC.rdoc at main · rack/rack

Webサーバーは「envハッシュ」と呼ばれるHTTPリクエストの情報が格納されたハッシュを用意し、それを引数としてcallメソッドを呼び出します。

Railsアプリケーションはcallメソッドの返り値として、以下の三つの値を格納した配列をWebサーバーに返します。

  • status : HTTPステータスコードを表す数値
  • headers : レスポンスヘッダーを含むハッシュ
  • body : レスポンスのボディとなるもので、通常は文字列の配列など

では、実際にどのような値が返ってくるのか確認してみましょう!

コンソール上で実験

Railsコンソール上で、Rails.applicationに対しcall`メソッドを呼び出してみます。

下準備として、rails new helloでアプリを新規作成し、/helloにアクセスするとHello, World!と表示される様にします。

helloと表示するアプリの画像

callメソッドに渡す「envハッシュは」、rack gemのRack::MockRequest.env_forを利用して用意します。

irb(main):001> env = Rack::MockRequest.env_for('http://localhost:3000/hello')
=> 
{"rack.input"=>#<StringIO:0x00000001077b1760>,
...

そのままcallメソッドに渡すとBlocked hostと言われてしまうため、「envハッシュ」にHTTP_HOSTヘッダーを追加します。

irb(main):002> env['HTTP_HOST'] = 'localhost'
=> "localhost"

では、「envハッシュ」を引数にRails.applicationに対してcallメソッドを呼び出します。

irb(main):003> status, headers, body = Rails.application.call(env)
Started GET "/hello" for  at 2023-12-10 15:31:40 +0900
Cannot render console from ! Allowed networks: 127.0.0.0/127.255.255.255, ::1
  ActiveRecord::SchemaMigration Load (0.3ms)  SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
Processing by HellosController#index as HTML
  Rendering layout layouts/application.html.erb
  Rendering hellos/index.html.erb within layouts/application
  Rendered hellos/index.html.erb within layouts/application (Duration: 0.2ms | Allocations: 209)
Everything's up-to-date. Nothing to do
  Rendered layout layouts/application.html.erb (Duration: 12.8ms | Allocations: 13220)
Completed 200 OK in 20ms (Views: 14.4ms | ActiveRecord: 0.0ms | Allocations: 17125)


MiniProfiler storage failure: no implicit conversion of nil into String
=> 
[200,

Completed 200 OKとあるので、このリクエストは成功した様ですね。

返り値を確認してみると、HTTPステータス、レスポンスヘッダー、レスポンスボディに対応した値が格納されていることがわかります。

irb(main):004> status
=> 200

irb(main):005> headers
=> 
{"x-frame-options"=>"SAMEORIGIN",
 "x-xss-protection"=>"1; mode=block",
 "x-content-type-options"=>"nosniff",
 "x-download-options"=>"noopen",
 "x-permitted-cross-domain-policies"=>"none",
 "referrer-policy"=>"strict-origin-when-cross-origin",
 "link"=>
  "</assets/application.debug-ead91a25923de99455378da7f1f1bb5a6839a249af27af911ec2b81709b046b7.css>; rel=preload; as=style; nopush,</packs/js/application-d4389a205e3a305a430b.js>; rel=preload; as=script; nopush",
 "content-type"=>"text/html; charset=utf-8",
 "etag"=>"W/\"cd556a718561e5e4f0c288ed64067335\"",
 "cache-control"=>"max-age=0, private, must-revalidate",
 "set-cookie"=>
  ["_hello_session=oNirYlMTT8MTrQFwUPjyqDIKnSZbe1SNGXcQMfesZ5FqJkYFwgpcfBeeGJ7sinDzFkrpGj40DUfjR9G8fOGHTys7Rx3fxmEa06ErCuNjge%2Bu1hWdjLpryWkBIElXgQF9dPBKvRJ2J2m50%2Bvhm5PRHfyy9Dp%2BN6fFvRxnuNtz2igBGQbqdVt9iNnW5Fl4zNlOM4516%2FQxiSQvtdAUhjGMRIQwwU3dHkjGMiYP0jZCPZCKq%2BoqlNDGke19aXZpiu5lealK4wA%2Fu3KIGv1KoEmuXla2tQcPLw%3D%3D--%2BaxhXWvOw%2FzjtOZL--bigBnV371hxh0QYi0EMAUw%3D%3D; path=/; httponly; SameSite=Lax",
   "__profilin=p%3Dt; path=/; httponly; SameSite=Lax"],
 "x-request-id"=>"f404e8ff-5504-4808-8e44-a87547285fab",
 "x-runtime"=>"0.048442"}

irb(main):006> headers
=>
#<Rack::BodyProxy:0x0000000107adc390
 @block=#<Proc:0x0000000107adc368 /Users/kashiyamashintarou/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/actionpack-7.1.2/lib/action_dispatch/middleware/executor.rb:15>,
 途中省略...
            @response=
            #<ActionDispatch::Response:0x0000000107d32630
             途中省略...
             @stream=
              #<ActionDispatch::Response::Buffer:0x0000000106dc0ff8
               @buf=
                ["<!DOCTYPE html>\n<html>\n  <head>\n    <title>Hello</title>\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n    <meta name=\"csrf-param\" content=\"authenticity_token\" />\n<meta name=\"csrf-token\" content=\"TVqQW_tF8fRvPSePyWVN10xc4vV8Fc-nwR8ffja8FhyYM_x59CsJ8KRxawd2VPBiL30Tf0xhAzxQarpFGZONjQ\" />\n    \n
\n    <link rel=\"stylesheet\" href=\"/assets/application.debug-ead91a25923de99455378da7f1f1bb5a6839a249af27af911ec2b81709b046b7.css\" media=\"all\" data-turbolinks-track=\"reloa
d\" />\n    <script src=\"/packs/js/application-d4389a205e3a305a430b.js\" data-turbolinks-track=\"reload\"></script>\n  </head>\n\n  <body>\n    <p>Hello World!</p>\n\n  </body>\
n</html>\n"],

Rackアプリケーションの仕様に基づいて、Rails.applicationがstatus・headers・bodyを返すことが分かりました👍

Rails.applicationのクラス

Rails.applicationはRackアプリケーションであることが分かりましたが、それはどの様なオブジェクトなのでしょうか?

クラスを確認してみます。

irb(main):001> Rails.application.class
=> Hello::Application

Hello::Applicationと表示されましたね。

Railsの起動プロセスについて調べてみると、次の様な記述がありました。

The application is also responsible for setting up and executing the booting process. From the moment you require config/application.rb in your app, the booting process goes like this:

  1. require "config/boot.rb" to set up load paths.
  2. require railties and engines.
  3. Define Rails.application as class MyApp::Application < Rails::Application.

Rails::Application

アプリケーション内でconfig/application.rbrequireした瞬間から起動プロセスが実行され、3番目に「Rails.applicationMyApp::Application < Rails::Applicationとして定義する」とあります。

起動プロセスを通じて、Rails.applicationアプリケーション名::Applicationとなることが分かりました。

config/application.rbを確認してみると、アプリケーション名::Applicationとなっていますね。

require_relative "boot"

require "rails/all"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module Hello
  class Application < Rails::Application
  #...

試しに名前空間を変更してみると、Rails.applicationのクラス名も変更されることが確認できます。

module GoodBye # HelloからGoodByeに変更
  class Application < Rails::Application
irb(main):001> Rails.application.class
=> GoodBye::Application

最後に、Rails.applicationの継承関係を見てみましょう。

祖先にはRails::EngineRails::Railtieというクラスが存在していることが分かりました。

irb(main):001> Rails.application.class
=> Hello::Application
irb(main):002> Rails.application.class.superclass
=> Rails::Application
irb(main):003> Rails.application.class.superclass.superclass
=> Rails::Engine
irb(main):004> Rails.application.class.superclass.superclass.superclass
=> Rails::Railtie
irb(main):005> Rails.application.class.superclass.superclass.superclass.superclass
=> Object

Rails.applicationの継承関係

終わりに

色々と調べる中で、Railsアプリケーションに対する解像度が少し上がった気がします。

Railsの中身を調べることは楽しいですね(コードを読んだり調べたりするのは大変ですが😇)。

今回の調査で十分理解できなかった箇所(Railsの起動プロセスの詳細など)は、レベルアップしてから再び調べることにします。

これからも自分の好奇心を大切に、興味を持ったことをどんどん深掘りしていきたいと思います!

参考文献