Rails 7.0 の api_only = true
で devise を使うと DisabledSessionError
で落ちるのをなんとかする。
概要
Rails 6.1 から 7.0 で ActionDispatch::Request::Session::DisabledSessionError
が 追加 されており、セッションが有効化されてない場合に使おうとした際に例外が投げられるようになった。これ自体は歓迎すべき変更だと思われる。
しかし api_only = true
で運用すると最小構成で Rails アプリが作られるため、該当するミドルウェアが読み込まれない。
devise が内部で暗黙的にセッションを使っている ため、セッションが無効なままここに到達すると warden が env["rack.session"]
を触って 例外が発生して死んでしまう。
対策を考える。
- Rails を止める。
- 身も蓋もない。
- Rails 7.0 を止める。
- まだしばらく 6.1 でもいいんじゃない、っていうのはある。
-
api_only = true
をやめる。- 身も蓋もない (2回目)
- ミドルウェアを挟んでセッションを有効にする。
- 今まで使ってなかったし最小限でやりたいのに余計なものを有効にするのは…という気持ちが強い。
- しかしながら
api_only = true
でもセッションを使う必要があって有効にしてた現場は割とありそう。 - そういうところでは、そもそもこの問題は発生してないはず。
- しかしながら
- 今まで使ってなかったし最小限でやりたいのに余計なものを有効にするのは…という気持ちが強い。
- ハックする。
- 一時凌ぎでいいし、これでいきましょう。
ハックの実装
困ってる人はいるようで、 heartcombo/devise/issues/5443 が立っており、議論の中で実装例を書いている人がいる。参考にしつつ実装してみることにする。
とりあえず request.env['rack.session']
に何か空の Hash でも放り込んでおけば良いので、こんな感じのダミーになりそうな class
を切る。
class FakeSession < Hash
def enabled?
false
end
def destroy
end
end
で、 request.env['rack.session'] ||= FakeSession.new
が実行されるようなメソッドを切って before_action
に放り込んでやればいい。
実際の実装は以下のような感じになると思う。
app/controllers/concerns/devise_hack_fake_session.rb
を切って
module DeviseHackFakeSession
extend ActiveSupport::Concern
class FakeSession < Hash
def enabled?
false
end
def destroy
end
end
included do
before_action :set_fake_session
private
def set_fake_session
if Rails.configuration.respond_to?(:api_only) && Rails.configuration.api_only
request.env['rack.session'] ||= ::DeviseHackFakeSession::FakeSession.new
end
end
end
end
で、 app/controllers/application_controller.rb
で読む。
class ApplicationController < ActionController::API
include DeviseHackFakeSession
# 略
end
上の様な対応を入れた プルリク も出てはいるのだが、これはあくまで一時凌ぎであって、この回避策はどう考えても雑であり devise 側が取り込むかというと極めて微妙な気がしている。
ちゃんとやるなら…
- devise 側で
warden.session_serializer
を触らないようにする。 - warden 側にセッションを使わないオプションを生やして使うようにする。
- 似たような議論が wardencommunity/warden/pull/201 でされていた。
みたいな方針が健全かなと思う。
ざっと見ただけだが、 devise 側から warden.session_serializer
を触ってるコードは
Devise::Controllers::SignInOut#sign_in
Devise::Controllers::SignInOut#bypass_sign_in
Devise::Test::ControllerHelpers#sign_in
Devise::Test::ControllerHelpers#sign_out
あたりなので、こちらを精査してパッチを充てた方がいいかもしれない。
api_only = true
で devise ってどういう状況やねん
devise_token_auth を使う場合とか、 devise-jwt を使う場合とかが考えられるかな…。
実際 devise-jwt 側でも issue が立っている。
他に DisabledSessionError
の追加によって出てくる問題とか。
なんと ActiveRecord ではマルチ DB 環境下において暗黙にセッションに依存しているところがあったようだ。
マルチDBでリードレプリカを有効にしたAPIモードのRails7において書き込み系リクエストが失敗する事象の対処方法と原因 に詳しい。
こんなの手元の開発環境で気付ける訳がない。もしステージング環境をしっかり作ってなかったら本番まで持っていって初めて死ぬ。
まぁしかし、どちらかというと今まで実は機能してなかったことに気付けたという意味ではやっぱり追加されてよかった例外なのではないでしょうか。
Rails 7.0 から DisabledSessionError
が追加された経緯を軽く追ってみる。
https://github.com/rails/rails/pull/42231
このプルリクが出たのが 2021/5/15 で、マージされたのが 2021/5/18
同じプルリクで 2 種類の DisabledSessionError
が足されており若干ややこしいが
-
ActionController::RequestForgeryProtection::DisabledSessionError
-
actionpack/lib/action_controller/metal/request_forgery_protection.rb
- 後に削除される。
-
-
ActionDispatch::Request::Session::DisabledSessionError
actionpack/lib/action_dispatch/request/session.rb
で、この時点では
config.action_controller.silence_disabled_session_errors = true
にすれば、いきなり例外で落ちるのではなくエラーメッセージを表示するだけで回避できそうに一瞬見えたけど、よく見ると違ってて ActionController::RequestForgeryProtection::DisabledSessionError
向けのオプションだった模様。
そして色々と問題が起きたのか ActionController::RequestForgeryProtection::DisabledSessionError
の方は 2021/10/11 のプルリク でごっそりと落とされ、ひっそりと残っていたオプションも 2021/12/7 のプルリク で落ちた。
Rails 7.0 のリリース日を見ると
- alpha1 と alpha2 が 2021/9/16
- rc1 が 2021/12/7 で、 rc2 と rc3 が 2021/12/15
- 7.0.0 が 2021/12/16
なので、最終的に ActionDispatch::Request::Session::DisabledSessionError
だけが残ってリリースされた模様。
締め
Rails に何を求めるのかという話なのだが、現代の Web アプリ開発的には単純に JSON さえ返してくれれば良くて、ブラウザの面倒まで見ないで欲しいというか、正直余計なことはしないで欲しい訳だけど、歴史的経緯からするとそうもいかない。
とはいえブラウザだけじゃなくてスマホアプリとかもある訳で、フロントエンドの面倒を見るという責務は分離されていた方が正直開発しやすい。
じゃあ Rails じゃなくて良くない?ってのは正論でもあるけど、現実の世の中では正論で殴ると殴られた方も殴った方も不幸になる訳です。難しいですね。