Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rescue_from not working with subscribed method on ActionCable channel #51855

Open
Buitragox opened this issue May 17, 2024 · 2 comments
Open

Comments

@Buitragox
Copy link

Buitragox commented May 17, 2024

When an error is raised in the subscribed method of an ActionCable channel, it does not get handled by the method registered in rescue_from. Other methods do get handled by their registered methods with rescue_from.

When running a Rails server, I found that the exception gets handled by the connection in the execute_command method, so using rescue_from for the exception in ApplicationCable::Connection catches the exception. I was not able to replicate this behavior in the test below.

Steps to reproduce

Raise an exception in Channel#subscribed and register a handler for this exception using rescue_from, just like the documentation says.

# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  gem "rails"
end

require "minitest/autorun"
require "action_cable"

ActionCable.server.config.cable ||= {}
ActionCable.server.config.cable["adapter"] = "test"
ActionCable.server.config.logger = Logger.new(STDOUT)

class BadSubscription < StandardError; end
class BadMessage < StandardError; end

module ApplicationCable
  class Channel < ActionCable::Channel::Base
  end
end

module ApplicationCable
  class Connection < ActionCable::Connection::Base
  end
end

class MessagesChannel < ApplicationCable::Channel
  rescue_from BadSubscription, with: :handle_bad_subscribe
  rescue_from BadMessage, with: :handle_bad_message

  def subscribed
    raise BadSubscription unless params.key?("id")
    stream_for "channel"
  end

  def receive(message)
    raise BadMessage unless message.key?('message')
    ActionCable.server.broadcast("channel", message)
  end

  private

  def handle_bad_subscribe
    puts 'Bad subcription caught'
    reject
  end

  def handle_bad_message
    puts 'Bad message caught'
  end

end

class BugTest < ActionCable::Channel::TestCase
  tests MessagesChannel

  # Uncaught exception
  def test_rejected_connection
    subscribe
    assert subscription.rejected?
  end
  
  # Passes because it handles the error
  def test_bad_message_no_broadcast
    subscribe id: 5
    perform :receive, not_a_message: "Hello, Rails!"
    assert_no_broadcasts('channel')
  end
end

Expected behavior

The exception raised in the subscribed method should be handled by handle_bad_subscribe, so the subscription is rejected.

Actual behavior

The exception is not caught and the test fails.

# Running:

Bad message caught
.E

Error:
BugTest#test_rejected_connection:
BadSubscription: BadSubscription
    test_case.rb:35:in `subscribed'
    test_case.rb:62:in `test_rejected_connection'

bin/rails test test_case.rb:61

Finished in 0.019191s, 104.2155 runs/s, 52.1078 assertions/s.
2 runs, 1 assertions, 0 failures, 1 errors, 0 skips

System configuration

Rails version: 7.1.3.2

Ruby version: 3.3.0

@zzak
Copy link
Member

zzak commented May 18, 2024

It looks like for channels, only public actions declared by the user are rescue-able and not callbacks like #subscribed:

# Extract the action name from the passed data and process it via the channel.
# The process will ensure that the action requested is a public method on the
# channel declared by the user (so not one of the callbacks like #subscribed).
def perform_action(data)
action = extract_action(data)
if processable_action?(action)
payload = { channel_class: self.class.name, action: action, data: data }
ActiveSupport::Notifications.instrument("perform_action.action_cable", payload) do
dispatch_action(action, data)
end
else
logger.error "Unable to process #{action_signature(action, data)}"
end
end

For connections since the test works, I think there isn't a problem here.
There is already a test for this:

test "accessing exceptions thrown during command execution" do
run_in_eventmachine do
setup_connection
subscribe_to_chat_channel
data = { "content" => "Hello World!", "action" => "throw_exception" }
@subscriptions.execute_command "command" => "message", "identifier" => @chat_identifier, "data" => ActiveSupport::JSON.encode(data)
exception = @connection.exceptions.first
assert_kind_of ChatChannelError, exception
end
end

Maybe we could add a test for not supporting rescue_from in non-user callbacks (like subscribed).

@Buitragox
Copy link
Author

only public actions declared by the user are rescue-able

Looks like only #subscribed and #unsubscribed are not rescue-able.

You can still use rescue_from by doing something like this:

def subscribed
    # ...
rescue StandardError => e
    rescue_with_handler(e)
end

But I still find it a bit confusing that these two actions are exceptions to rescue_from, as their behavior is defined inside the channel.

This is the current #subscribe_to_channel method which calls #subscribed:

def subscribe_to_channel
run_callbacks :subscribe do
subscribed
end
reject_subscription if subscription_rejected?
ensure_confirmation_sent
end

Maybe using rescue_with_handler in this method could be a good solution?

def subscribe_to_channel
  run_callbacks :subscribe do
    subscribed
  end
rescue Exception => exception
  rescue_with_handler(exception) || raise
ensure
  reject_subscription if subscription_rejected?
  ensure_confirmation_sent
end

And something similar with #unsubscribe_from_channel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants