🦧

redirect_toとsend_dataを同時に呼ぶとエラーが出る理由"

2022/10/01に公開

コントローラーの同一メソッド内でredirect_toとsend_dataを同時に呼ぶとエラーが出る

send_dataした後にリダイレクトする処理を書いたアクションを用意しルーティングを設定します。

class SampleController < ApplicationController
  def hoge
    send_data('hoge', filename: 'hoge.txt')
    render html: "<h2>hello world</h2>".html_safe
  end
end

その上でページに遷移するとAbstractController::DoubleRenderErrorが出ます。

Render and/or redirect were called multiple times in this action.
Please note that you may only call render OR redirect, and at most once per action.
Also note that neither redirect nor render terminate execution of the action,
so if you want to exit an action after redirecting, you need to do something like "redirect_to(...) and return".

これはsend_data、redirect_toメソッドが両方ともレスポンスを返す処理であり、二重にレスポンスを返す処理を書いていることになるためです。

詳しくエラーメッセージを見てみると

Please note that you may only call render OR redirect, and at most once per action.

とあります。しかしアクション内でredirect_toメソッドは一度しか使用しておらずrenderメソッドに至っては呼ばれていません。一度調査してみましょう。

調査

予想

send_dataが内部的にrenderメソッドを呼び出している、とかであれば2回renderメソッドが呼ばれてエラーになることに辻褄が合いそうです。

binding.irbで調査

bindng.irbを挟んで調査します。

app/controllers/posts_contollers.rb
class SampleController < ApplicationController
  def hello
    binding.irb
    send_data('hoge', filename: 'hoge.txt')
    render html: "<h2>hello world</h2>".html_safe
  end
end

method(:send_data).source_locationでメソッドが定義されている場所を特定すると次のコードが見つかりました。

https://github.com/rails/rails/blob/f8e97a1464e0ab7feabf87f9da7fd9a86af509a0/actionpack/lib/action_controller/metal/instrumentation.rb#L34-L38

superが呼ばれているのでモジュールまたは継承クラスに定義されている同名のインスタンスメソッドを探す必要があります。
そこでbinding.irb中で以下のような処理を実行します。

# bingig.irbの中
self.class.ancestors.each do |m|
  i_methods = []
  # モジュールまたはクラスの継承していないインスタンスメソッドを取り出す
  i_methods << m.instance_methods(false)
  i_methods << m.private_instance_methods(false)

  # send_dataインスタンスメソッドを持っているモジュールまたはクラス
  puts m if i_methods.map(&:to_s).grep(/send_data/).present?
end

実行すると以下のモジュールが該当することがわかりました。
ActionController::Instrumentationは既に見ているのでActionController::DataStreamingを調べます。
ActionController::DataStreaming.instance_method(:send_data).source_locationでメソッドの定義場所を特定します。

やはり予想通りrenderが呼ばれていました。めでたしめでたし。

https://github.com/rails/rails/blob/f8e97a1464e0ab7feabf87f9da7fd9a86af509a0/actionpack/lib/action_controller/metal/data_streaming.rb#L109-L112

GitHubで編集を提案

Discussion