: O. Yuanying

ActionController::UrlWriter#url_forを色んなところで使うのは注意

最近、railsで作った自作ブログウェアでコメントが付けられなくなってました。何故かと言うとコメントを受け付けるurlがおかしかったからなんです。

なんでおかしかったか調べてみるとActionController::UrlWriter#url_forActionController::Base#url_forの動作が異なってたから。

ってか、これってrailsのバグなんじゃね!?とか言ってみる。

ActionController::UrlWriter#url_for

ユニットテストから link_to や url_for を使う方法 - Rails で行こう! - Ruby on Rails を学ぶ」や「 83's : url_forが使えないところで使えるようにする」の記事を見る限り、ActionController::UrlWriter#url_forはControllerとView以外でやむなくurl_forが使いたくなった時に利用するモジュールらしい。

ソースコードを見てみると、こんな感じでurlを組み立ててる。

def url_for(options)
      options = self.class.default_url_options.merge(options)
      
      url = ''
      unless options.delete :only_path
        url << (options.delete(:protocol) || 'http')
        url << '://'
        
        raise "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" unless options[:host]
        url << options.delete(:host)
        url << ":#{options.delete(:port)}" if options.key?(:port)
      else
        # Delete the unused options to prevent their appearance in the query string
        [:protocol, :host, :port].each { |k| options.delete k }
      end
      anchor = "##{options.delete(:anchor)}" if options.key?(:anchor)
      url << Routing::Routes.generate(options, {})
      return "#{url}#{anchor}"
    end
  1. options{:protocol] || 'http'
  2. '://'
  3. options[:host]
  4. options[:port]
  5. Routing::Routes.generate(options, {})
  6. "##{options.delete(:anchor)}"

おー、urlが組み立てられてるよー。

ってことで早速自分のクラスでinclude ActionController::UrlWriterしてurl_forしてみました。

ところが、開発環境ではうまくいってたのですが、いざ本番環境で試してみるとうまくいかない。

期待したurlはこんな感じでした。

/rana3/comment/post

けど、実際に組み立てられたurlはこんな感じ。

/comment/post

そう、本番環境ではrails用のドメインを作るのがめんどかったのでmongrelの--prefixオプションを使ってrelative_url_rootを利用してたのです。まあ要するに[Rails] Rails の動作環境と Location について - プログラミングは素晴らしいで説明してるようなことをやってた訳です。

ところが、実際に組み立てられたurlがおかしい…。

ActionController::Base#url_for

普通にrailsアプリ中のコントローラから呼び出したurl_forはちゃんと動いてるのにおかしいなーとちょっとそのソースコードを見てみると。

def url_for(options = {}, *parameters_for_method_reference) #:doc:
        case options
          when String
...
(中略)
...
          when Hash
            @url.rewrite(rewrite_options(options))
        end
      end

@url.rewriteを呼び出してますね…。@urlがどんなクラスのインスタンスなのかを調べてみると、どうやらActionController::UrlRewriterのインスタンスの模様。

じゃあ、@url.rewriteは、何をやってるのかと調べてみると。

def rewrite(options = {})
      rewrite_url(rewrite_path(options), options)
    end

...

    def rewrite_url(path, options)
      rewritten_url = ""
      unless options[:only_path]
        rewritten_url << (options[:protocol] || @request.protocol)
        rewritten_url << (options[:host] || @request.host_with_port)
      end

      rewritten_url << @request.relative_url_root.to_s unless options[:skip_relative_url_root]
      rewritten_url << path
      rewritten_url << '/' if options[:trailing_slash]
      rewritten_url << "##{options[:anchor]}" if options[:anchor]

      rewritten_url
    end

あれ、ActionController::UrlWriterの姿形が見えません!

組み立て方は、、

  1. options[:protocol]
  2. options[:host]
  3. @request.relative_url_root.to_s
  4. path
  5. '/'
  6. "##{options[:anchor]}"

ActionController::UrlWriter#url_forと別物ですね…。ちゃんとrelative_url_rootも考慮してるし!

結論

というわけで、relative_url_rootを利用してる時にActionController::UrlWriter#url_forを利用するのは要注意と言う事で!

その他

railsではたしてどんなところでActionController::UrlWriterを利用してるのか気になったので調べてみるとActionMailer::Baseでincludeしてました。

ってことはrelative_url_rootを利用してるアプリケーションでrailsからメール使うとまずいんじゃないかと思うのですが…。

まあよく調べてはいないので他のところで修正されてるのかもしれませんが。

参考