subscribe コマンドで channel が欠如した場合に ChannelNotFound を発生させる修正
ActionCable のサブスクリプション管理において、channel キーが欠けた identifier が送られた際に、期待された ChannelNotFound エラーではなく NoMethodError が発生していた問題を解消します。
背景
ActionCable::Connection::Subscriptions#subscription_from_identifier は identifier を JSON デコードし、id_options[:channel] を safe_constantize でクラスへ変換します。クライアントが {"id":1} のように channel を省略すると、id_options[:channel] が nil となり nil.safe_constantize が呼び出され NoMethodError がスローされました。この例外は add パスで既に実装されている ChannelNotFound と意味が異なり、エラーハンドリングの一貫性を損ねていました。
技術的な変更
コードのガード追加
subscription_from_identifier 内のクラス取得ロジックを安全航路(safe navigation)に変更し、channel が無い場合は nil を返すようにしました。
@@
- subscription_klass = id_options[:channel].safe_constantize
+ subscription_klass = id_options[:channel]&.safe_constantize
この単一行の差分により、id_options[:channel] が nil のとき &. 演算子が評価され、safe_constantize が呼び出されずに nil が返ります。以降の if subscription_klass && ActionCable::Channel::Base > subscription_klass 判定に引っ掛かり、else ブランチで ChannelNotFound がスローされます。
テストの追加
欠損した channel を含む subscribe コマンドに対し、正しく ChannelNotFound が発生することを確認するテストを追加しました。
@@
test "subscribe command without a channel" do
setup_connection
identifier = ActiveSupport::JSON.encode(id: 1)
assert_raises ActionCable::Connection::Subscriptions::ChannelNotFound do
@subscriptions.execute_command "command" => "subscribe", "identifier" => identifier
end
assert_empty @subscriptions.identifiers
end
これにより、レグレッション防止が自動化され、変更の正当性が検証可能となります。
設計判断
最小侵入で一貫したエラーフローを実現
&. 演算子の導入は既存ロジックへの影響を最小限に抑えつつ、channel が欠如したケースでも同一の例外経路へ誘導します。新たな設定項目や分岐を増やさず、ChannelNotFound の生成位置を統一した点は、コードベース全体のエラーハンドリングポリシーと整合性を保つ設計判断と言えます。
後方互換性の維持
id_options[:channel] が文字列で正しく解決できる従来ケースは、変更前後で振る舞いが変わりません。nil に対する安全ガードのみを追加したため、既存のアプリケーションコードに影響を与えることはありません。
まとめ
subscription_from_identifier に安全ナビゲーションを導入し、channel キーが欠如した identifier が送られた場合でも ChannelNotFound が一貫してスローされるようになりました。追加テストにより回帰が防止され、エラーハンドリングの一貫性が向上します。