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_rootRails.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 バージョンアップの際には,またハマると思います. その時にこの経験が役に立つかもしれません.