Rails 8.1で`ActionMailer::Base.mail`が`ActionNotFound`を引き起こすリグレッションを修正
Rails 8.1でActionMailer::Base.mailをクラスレベルで直接呼び出すとAbstractController::ActionNotFoundが発生するリグレッションが修正されました。クラスレベルにmailメソッドとaction_methodsの上書きを追加することで、Rails 8.0の動作が復元されます。
背景
Rails 8.1ではActionMailer::Base.mailが「アクション」として認識されなくなり、直接呼び出した際にAbstractController::ActionNotFoundが発生するリグレッションが混入しました。このバグは#56449として報告されています。
問題を再現するコードは以下のとおりです。
ActionMailer::Base.mail(
from: 'test@test.com',
to: 'test@test.com',
subject: 'Test',
body: 'test'
).deliver
これを実行すると、AbstractController::Base#processがmailをアクションとして発見できず、以下のエラーが発生していました。
The action 'mail' could not be found for ActionMailer::Base (AbstractController::ActionNotFound)
サブクラスを定義してメーラーを作成する通常のユースケースでは問題が起きませんが、ActionMailer::Baseを直接利用する使い方はシンプルなスクリプトや既存コードで広く使われており、影響範囲は軽視できません。
技術的な変更
actionmailer/lib/action_mailer/base.rbのクラスメソッドとしてmailとaction_methodsの2つが追加されました。
変更後:
def mail(...)
MessageDelivery.new(self, :mail, ...)
end
def action_methods
methods = super
methods.add("mail") if self == ActionMailer::Base
methods
end
クラスレベルのmailメソッドは、self(ActionMailer::Baseクラス自身)と:mailシンボルを引数としてMessageDelivery.newを呼び出し、受け取ったキーワード引数をそのまま転送します。これにより、サブクラスを介さずにMessageDeliveryオブジェクトを生成できます。
action_methodsの上書きでは、self == ActionMailer::Baseの場合に限り"mail"をセットに追加します。サブクラスではsuperの結果をそのまま返すため、既存のサブクラスの動作に影響を与えません。この条件付き追加こそが、AbstractController::Base#processがmailをアクションとして発見できるようにする核心的な変更です。
テスト面では、actionmailer/test/base_test.rbに2つのテストが追加されています。1つはdeliver_nowまで実行してデリバリー件数とsubjectを検証するもの、もう1つはMessageDeliveryオブジェクトの内容を検証するものです。また、既存のテストでassert_not_respond_to BaseMailer, :mailだった行がassert_respond_to BaseMailer, :mailに変更されており、サブクラスでもmailがパブリックに呼び出せることが保証されています。
設計判断
ActionMailer::Baseクラスにのみ"mail"を追加する方式が採用されました。
action_methodsの上書きでself == ActionMailer::Baseという条件が設けられているのは、サブクラスにまでmailをアクションとして公開しないためです。サブクラスではユーザー定義のアクションメソッドだけがアクションとして機能すべきであり、基底クラスのディスパッチ用メソッドが混入するとルーティングや意図しないアクション呼び出しが生じる恐れがあります。最小限の条件分岐で対象を限定することで、既存のサブクラス利用への副作用を排除しています。
クラスレベルのmailメソッドがMessageDelivery.new(self, :mail, ...)を返す実装も注目に値します。サブクラスのアクションと同じMessageDeliveryインターフェースを返すことで、deliver_nowやdeliver_laterなどの呼び出しがそのまま機能します。
まとめ
本PRはActionMailer::Baseのクラスメソッド2つを追加するだけの小さな変更で、Rails 8.0からの後退を解消しています。action_methodsを条件付きで拡張することで、基底クラスの直接利用と既存サブクラスの振る舞いを同時に保証するアプローチは、Abstract Controllerのアクション検出機構への深い理解に基づいた判断といえます。