Active StorageのベースコントローラーにAPIオンリー対応の親クラス設定を追加
APIオンリーアプリ向けに、ActiveStorage::BaseControllerの親クラスをconfig.active_storage.base_controller_parentで設定できるようになりました。config.api_only = trueの場合は自動的にActionController::APIが使われるため、これまで回避策が必要だったCSRF検証エラーやOriginヘッダー不一致の問題が解消されます。
背景
APIオンリーのRailsアプリでActive Storageのダイレクトアップロードを使用すると、ActiveStorage::BaseControllerがActionController::Baseを継承しているために問題が発生していました。#32208で報告された具体的なエラーは次の2点です:
HTTP Origin header didn't match request.base_urlCan't verify CSRF token authenticity.
これらはCSRF保護やオリジン検証を前提としたActionController::Base固有の仕組みに起因するものです。APIオンリーアプリのApplicationControllerはActionController::APIを継承しているため、ActiveStorageコントローラーの親クラスと前提が食い違っていました。
これまでのワークアラウンドとしては、ActiveStorage::DirectUploadsControllerを継承したカスタムコントローラーを作成し、親クラスを差し替える方法が取られていましたが、Rails本体の設定として対応する公式な手段がありませんでした。
技術的な変更
ActiveStorage.base_controller_parentという新しいモジュール属性が追加され、ActiveStorage::BaseControllerはこの値を元に動的に親クラスを決定するようになりました。
activestorage/lib/active_storage.rbへの追加:
mattr_accessor :base_controller_parent, default: "::ActionController::Base"
activestorage/app/controllers/active_storage/base_controller.rbの変更:
# 変更前
class ActiveStorage::BaseController < ActionController::Base
include ActiveStorage::SetCurrent
protect_from_forgery with: :exception
self.etag_with_template_digest = false
end
# 変更後
class ActiveStorage::BaseController < ActiveStorage.base_controller_parent.constantize
include ActiveStorage::SetCurrent
protect_from_forgery with: :exception if respond_to?(:protect_against_forgery?)
self.etag_with_template_digest = false if respond_to?(:etag_with_template_digest)
end
親クラスを文字列として保持し.constantizeで解決する方式により、Engineの初期化順序に依存しない安全な定数解決が可能になっています。また、protect_from_forgeryとetag_with_template_digestの呼び出しにrespond_to?ガードが追加され、ActionController::APIのように対応するメソッドを持たない親クラスでも安全に動作するようになっています。
activestorage/lib/active_storage/engine.rbでの自動設定:
ActiveStorage.base_controller_parent = app.config.active_storage.base_controller_parent ||
if app.config.api_only
"::ActionController::API"
else
"::ActionController::Base"
end
Engineの初期化時にconfig.api_onlyの値を参照し、明示的な指定がない場合は自動的に適切な親クラスを選択します。ユーザーがconfig.active_storage.base_controller_parentを明示した場合はその値が優先されるため、ActionController::BaseとActionController::API以外の独自の親クラスも指定できます。
activestorage/app/controllers/active_storage/disk_controller.rbの変更:
# 変更前
skip_forgery_protection
# 変更後
if respond_to?(:skip_forgery_protection)
skip_forgery_protection
end
DiskControllerでも同様にrespond_to?ガードが追加されており、ActionController::APIを親クラスとする場合にNoMethodErrorが発生しないよう対処されています。
設計判断
クラス名を文字列として保持し.constantizeで解決する方式が採用されました。mattr_accessorのdefault値に文字列を使うことで、Engineがロードされる前の時点で定数参照が発生するのを避けています。
設定の優先順位は「明示的なbase_controller_parent指定 → api_onlyフラグによる自動選択 → ::ActionController::Base」の3段階になっています。これにより、既存の非APIアプリにはデフォルト値のまま何も変更せずに動作する後方互換性が確保されています。
respond_to?によるメソッド存在確認はコントローラー定義時(クラスボディ評価時)に実行されるため、ランタイムのオーバーヘッドはなく、親クラスの能力に応じた機能の有効化を静的に制御できる点が実用的な設計といえます。
まとめ
本PRは、APIオンリーアプリにおけるActive Storageの長年の課題を、Engineレベルの設定とコントローラーのrespond_to?ガードという最小限の変更で解決しています。既存アプリへの影響をゼロに保ちながら、config.api_onlyによる自動適用とbase_controller_parentによる手動上書きの両方を提供する設計は、Railsの設定哲学に沿った実装といえます。