`before_subscribe` コールバックで `#reject` を呼んだ際に `#subscribed` をスキップするよう修正
Action Cable の before_subscribe コールバック内で #reject を呼び出しても #subscribed が実行されてしまうバグが修正されました。これにより、before_action でレンダリングした場合にアクションがスキップされるコントローラの挙動と同様の一貫した動作が実現されます。
背景
before_subscribe コールバックで #reject を呼び出しても、後続の #subscribed メソッドが実行されるという不整合な挙動が報告されていました(#52229)。
以下のように before_subscribe でサブスクリプションを拒否するチャンネルを定義した場合、#subscribed は呼び出されないことが期待されます:
class ChatChannel < ApplicationCable::Channel
before_subscribe do
reject
end
def subscribed
Rails.logger.info("subscribed!") # 本来は呼ばれてはいけない
end
end
しかし実際には subscribed! がログに出力されていました。通常のコントローラで before_action がレンダリングやリダイレクトを行った際にアクションメソッドがスキップされるのとは対照的な挙動であり、設計上の一貫性を欠いていました。
技術的な変更
変更の核心は ActionCable::Channel::Base#subscribe_to_channel の1行の修正です。#subscribed の呼び出しに unless subscription_rejected? の条件を追加することで、before_subscribe コールバック実行後にサブスクリプションが拒否済みであれば #subscribed をスキップします。
変更前:
def subscribe_to_channel
run_callbacks :subscribe do
subscribed
end
reject_subscription if subscription_rejected?
ensure_confirmation_sent
end
変更後:
def subscribe_to_channel
run_callbacks :subscribe do
subscribed unless subscription_rejected?
end
reject_subscription if subscription_rejected?
ensure_confirmation_sent
end
run_callbacks :subscribe ブロック内で before コールバック(before_subscribe)が先に実行されるため、その時点で subscription_rejected? を評価することで、#reject が呼ばれていれば #subscribed の実行を防止できます。reject_subscription および ensure_confirmation_sent の呼び出し順序は変わらないため、拒否通知のクライアントへの送信フローには影響しません。
合わせて actioncable/lib/action_cable/channel/callbacks.rb の before_subscribe メソッドのドキュメントも更新されました。#reject を呼び出した場合に #subscribed がスキップされる旨が明記されています。
# This callback will be triggered before the Base#subscribed method is called.
#
# However, if the subscription is rejected with the Base#reject method in any
# such callback, the Base#subscribed method will not be called.
#
def before_subscribe(*methods, &block)
set_callback(:subscribe, :before, *methods, &block)
end
設計判断
subscribed の条件付きスキップ という最小限の変更が採用されています。
run_callbacks の :subscribe フックは before → 本体 → after の順に実行されます。before_subscribe コールバックが #reject を呼び出すと subscription_rejected? が true になるため、本体(subscribed)の実行をその場でガードするだけで目的が達成できます。コールバックチェーン自体を中断するのではなく、本体をスキップする判断が取られており、after_subscribe など他のコールバックへの影響を最小化しています。
テストでは RejectBeforeSubscribeChannel を定義し、subscribe_to_channel 呼び出し後に channel.room が nil であること、および subscribed? が false であることを検証しています。また、テストスタブの test_socket.rb に ActionCable::Connection::Subscriptions の初期化が追加され、テストインフラも整備されています。
まとめ
今回の修正は subscribed unless subscription_rejected? という1行の追加に留まりながら、Action Cable のコールバックと通常のコントローラの before_action における振る舞いの一貫性を回復させています。before_subscribe を認証・認可のゲートとして利用するパターンが、設計上の期待通りに機能するようになりました。