Rails with Relative URL Root
はじめに
Rails アプリケーションは,通常 /
を URL root として動作します.
この記事は,これを /myapp
に変更したときの顛末です.
Rails のバージョンは 3.2.6 です.
結論となる対処法は簡単なのですが,
事情は思ったより複雑のようです.Rails は,
正しい作法がすぐ変わるので混乱しますね.
結論だけ知りたい人は,
結局おすすめは の節だけ読めばいいです.
まずは公式のガイドから
Ruby on Rails Guides: Configuring Rails Applications によると,以下の2つが目的に関係しそうです.
Rails.application.config.relative_url_root
Rails.application.config.assets.prefix
relative_url_root
は,環境変数 RAILS_RELATIVE_URL_ROOT
によってデフォルト値が設定されるので,
% RAILS_RELATIVE_URL_ROOT='/myapp' rails server -p 3000
などとすると,反映されるように思えます.しかし,rake で確認してみると,
% RAILS_RELATIVE_URL_ROOT='/myapp' rake routes new_user_session GET /users/sign_in(.:format) devise/sessions#new user_session POST /users/sign_in(.:format) devise/sessions#create destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy content_material GET /materials/:id/content(.:format) materials#content materials GET /materials(.:format) materials#index POST /materials(.:format) materials#create new_material GET /materials/new(.:format) materials#new edit_material GET /materials/:id/edit(.:format) materials#edit material GET /materials/:id(.:format) materials#show PUT /materials/:id(.:format) materials#update DELETE /materials/:id(.:format) materials#destroy root / materials#index
何だかうまくいってない.
ググってみると,最近の Rails では, Rails.application.config.relative_url_root
を陽に参照するように config/routes.rb
を書換えないとダメみたいです.
そこで, config/application.rb
に scope の記述を追加:
Myapp::Application.routes.draw do scope (Rails.application.config.relative_url_root || '/') do # ... end end
これで,
% RAILS_RELATIVE_URL_ROOT='/myapp' rake routes new_user_session GET /myapp/users/sign_in(.:format) devise/sessions#new user_session POST /myapp/users/sign_in(.:format) devise/sessions#create destroy_user_session DELETE /myapp/users/sign_out(.:format) devise/sessions#destroy content_material GET /myapp/materials/:id/content(.:format) materials#content materials GET /myapp/materials(.:format) materials#index POST /myapp/materials(.:format) materials#create new_material GET /myapp/materials/new(.:format) materials#new edit_material GET /myapp/materials/:id/edit(.:format) materials#edit material GET /myapp/materials/:id(.:format) materials#show PUT /myapp/materials/:id(.:format) materials#update DELETE /myapp/materials/:id(.:format) materials#destroy root /myapp(.:format) materials#index
おお,うまくいった!! と思いきや. Rails.application.config.assets.prefix
は連動しない様子.
assets 関係は,別のサーバに置いたりするからでしょうか.サーバを1つで済ませたいので,
config/application.rb
に assets の場所を記述します.
config.assets.prefix = File.join((config.relative_url_root || '/'), 'assets')
今度こそうまくいった!!
Devise がおかしい
でも,認証用の Devise プラグインがうまくいかないです.具体的には,未認証時の redirect 先である
new_user_session_path
がおかしい. 先の rake routes
の結果からすると
/myapp/users/sign_in
に redirect されそうなのに,実際には,
/myapp/myapp/users/sign_in
に飛ばされてしまって 404 が出ます.
ただ,URL 直叩きで /myapp/users/sign_in
にアクセスすれば OK です.つまり,redirect
先の決定部分だけがおかしい.
Rack の 認証フレームワーク Warden からたどることしばし.認証失敗時の
redirect 先を決めているのは, lib/devise/failure_app.rb
の scope_path
だと分かりました.
以下抜粋.
1: def scope_path 2: opts = {} 3: route = :"new_#{scope}_session_path" 4: opts[:format] = request_format unless skip_format? 5: 6: config = Rails.application.config 7: opts[:script_name] = (config.relative_url_root if config.respond_to?(:relative_url_root)) 8: 9: context = send(Devise.available_router_name) 10: 11: if context.respond_to?(route) 12: context.send(route, opts) 13: elsif respond_to?(:root_path) 14: root_path(opts) 15: else 16: "/" 17: end 18: end
7行目で config.relative_url_root
を SCRIPT_NAME
に突込んでるのが原因のようです.
ここをコメントアウトすると動きます.これで問題解決としてもよかったのですが,
何かよく意味が分からないので,もう少し調べてみました.
7行目の動作って,大きなお世話じゃないの??… と思ったけど.調べると結構面倒な問題みたいで, Devise に関係なく,Rails 本家の github でもいくつか議論があった様子です. 以下が事情をよく説明しているように思えます.
config.action_controller.relative_url_root doesn't work in Rails 3.1 · Issue #4308 · rails/rails
SCRIPT_NAME
vs RAILS_RELATIVE_URL_ROOT
僕の理解では,こういう問題があるようです.
- Rack の枠組では,
SCRIPT_NAME
が Rails でいうところのrelative_url_root
と同じ意味を持つ. しかし,両者は,連携しているようでしていない. - Rack サーバの WebRick は,両者の関係を意識しないが, thin とかは,
--prefix /myapp
によってrelative_url_root
を設定する他,リクエスト毎のSCRIPT_NAME
やPATH_INFO
を設定する(ような気がする). - assets 関係は,登場後日が浅い上にアプリケーションのルーティング (
config/routes.rb
) の外にあるので, 更に半端な連携状態になっている.
URL(リンク)の生成時と,対応するリソースの配信時(サーブ)で異なる情報を使っていることが問題のようです.
Devise は,そのへんの微妙な関係を修復するために世話を焼いていたようです.
本来なら Rack アプリケーションとしての Rails は, SCRIPT_NAME
を信じて動けば問題ない筈で,
RAILS_RELATIVE_URL_ROOT
はなくてもいいように思うのですが,
過去の経緯からなのか,半端な状態になっているんだと思います.
Rack に正しく SCRIPT_NAME
を付けさせるには,
Rack に対応したアプリケーションを起動する際に map
で囲めばいいようです.
単純な Rack アプリケーションで確認してみましょう hello-rack.rb
:
require 'pp' class HelloRack def call(env) pp env [ 200, { 'Content-Type' => 'text/plain' }, ['Hello Rack'] ] end end
config.ru
:
require ::File.expand_path('.', 'hello-rack.rb') map "/myapp" do run HelloRack.new end
実行して
% rackup
localhost:9292
にアクセスすると,
{ "SCRIPT_NAME"=>"/myapp", # diff "PATH_INFO"=>"/hello", # diff "QUERY_STRING"=>"", "REQUEST_URI"=>"http://localhost:9292/myapp/hello", "SERVER_NAME"=>"localhost", "SERVER_PORT"=>"9292", "SERVER_SOFTWARE"=>"WEBrick/1.3.1 (Ruby/1.9.3/2012-02-16)", "HTTP_HOST"=>"localhost:9292", "REQUEST_PATH"=>"/myapp/hello" }
一方, config.ru
に map
がない場合は,
{ "SCRIPT_NAME"=>"", # diff "PATH_INFO"=>"/myapp/hello", # diff "QUERY_STRING"=>"", "REQUEST_URI"=>"http://localhost:9292/myapp/hello", "SERVER_NAME"=>"localhost", "SERVER_PORT"=>"9292", "SERVER_SOFTWARE"=>"WEBrick/1.3.1 (Ruby/1.9.3/2012-02-16)", "HTTP_HOST"=>"localhost:9292", "HTTP_VERSION"=>"HTTP/1.1", "REQUEST_PATH"=>"/myapp/hello" }
SCRIPT_NAME
と PATH_INFO
の関係が map
によって変化しています.
Rack アプリケーションは, SCRIPT_NAME
を加味して URL を生成しつつ,
PATH_INFO
を見てルーティングすればいいようです.
結局おすすめは
で,結局 Stack Overflow に頼るのが一番です. ruby on rails - What is the replacement for ActionController::Base.relative_url_root? - Stack Overflow つまり,以下の2つを両方実行すればいいです.
config.ru の run を map で包む
map ActionController::Base.config.relative_url_root || "/" do run Myapp::Application end
これによって,毎回のリクエストに
SCRIPT_NAME
が設定され, これを利用してアプリケーションのルーティング (routes) 関連がうまく振舞えるようになります.環境変数
RAILS_RELATIVE_URL_ROOT
を設定してサーバを起動する% RAILS_RELATIVE_URL_ROOT='/myapp' rails server -p 3000
これによって,Devise が
SCRIPT_NAME
をRAILS_RELATIVE_URL_ROOT
で上書きしても値が同じになるので, 未認証時の redirect がうまく動作します.
おまけ
この2つを実行すると,おまけで assets 関連もうまくサーブされるようになります.
つまり, config.assets.prefix
の設定は不要です.
上記2つの一方だけでは,assets は,うまくサーブされません.どうも,現状は,こうなっているようです:
RAILS_RELATIVE_URL_ROOT
をセットすると,asset 関連のURL生成部が/myapp
を prefix として付けるようになる. しかし,この設定は,サーブ側には働かない.- サーブ側を動作させるためには,
config.assets.prefix
で陽に指定するか, Rack 側の設定でmap
を書いて,SCRIPT_NAME
とPATH_INFO
を正しく与えるようにする.
この問題は,おそらく,次期バージョンで解決しそうです.
Merge pull request #5296 from dlitz/relative_url_root_from_script_name · 9beb0b6 · rails/rails
つまり,assets のために RAILS_RELATIVE_URL_ROOT
を設定する必要がなくなりそうです.
一方, routes に関してはどうでしょうか.routes は, RAILS_RELATIVE_URL_ROOT
を URL生成時に使わないようです.
これは,assets とちぐはぐに見えますね.
/myapp
をURL生成時に付けさせるためには,以下2つのいずれかが必要です.
config/routes.rb
中にscope hoge...
の記述をする.- Rack 側の設定で
map
を書いて,SCRIPT_NAME
とPATH_INFO
を正しく与えるようにする.
後者でURL生成がうまくいく理由がよく分かりませんが,おそらくURL生成にも SCRIPT_NAME
を見てるんでしょうね.
両者は,若干意味が違います.
前者は,routes が /myapp
まで意識してルーティングしているのに対して,
後者は,Rack が /myapp
を意識していて, routes は PATH_INFO
を見て /
をトップとしてルーティングしています.
なので,上記2つともやると,routes が GET /
は知らんといって怒ります.
ルーティング関連は,よく分からないことだらですね.Rails バージョンアップの際には,またハマると思います. その時にこの経験が役に立つかもしれません.