From d2571e560c62116f60429c933d0c41a0e249b58b Mon Sep 17 00:00:00 2001 From: justin talbott Date: Fri, 20 Mar 2020 13:35:10 -0400 Subject: [PATCH] add rescue_with support to ActionCable::Connection::Base and update ActionCable guide to describe exception handling usage # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # # On branch master # Your branch is behind 'origin/master' by 5 commits, and can be fast-forwarded. # # Changes to be committed: # modified: actioncable/CHANGELOG.md # modified: actioncable/lib/action_cable/connection/base.rb # modified: actioncable/lib/action_cable/connection/subscriptions.rb # modified: actioncable/test/connection/subscriptions_test.rb # modified: guides/source/action_cable_overview.md # --- actioncable/CHANGELOG.md | 6 +++ .../lib/action_cable/connection/base.rb | 2 + .../action_cable/connection/subscriptions.rb | 1 + .../test/connection/subscriptions_test.rb | 32 ++++++++++++++- guides/source/action_cable_overview.md | 40 +++++++++++++++++++ 5 files changed, 80 insertions(+), 1 deletion(-) diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md index b020868a75..c504e7058a 100644 --- a/actioncable/CHANGELOG.md +++ b/actioncable/CHANGELOG.md @@ -1,3 +1,9 @@ +* `ActionCable::Connection::Base` now allows intercepting unhandled exceptions + with `rescue_from` before they are logged, which is useful for error reporting + tools and other integrations. + + *Justin Talbott* + * Add `ActionCable::Channel#stream_or_reject_for` to stream if record is present, otherwise reject the connection *Atul Bhosale* diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb index c469f7066c..a243a91def 100644 --- a/actioncable/lib/action_cable/connection/base.rb +++ b/actioncable/lib/action_cable/connection/base.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "action_dispatch" +require "active_support/rescuable" module ActionCable module Connection @@ -46,6 +47,7 @@ class Base include Identification include InternalChannel include Authorization + include ActiveSupport::Rescuable attr_reader :server, :env, :subscriptions, :logger, :worker_pool, :protocol delegate :event_loop, :pubsub, to: :server diff --git a/actioncable/lib/action_cable/connection/subscriptions.rb b/actioncable/lib/action_cable/connection/subscriptions.rb index 1ad8d05107..41b4cc4e92 100644 --- a/actioncable/lib/action_cable/connection/subscriptions.rb +++ b/actioncable/lib/action_cable/connection/subscriptions.rb @@ -21,6 +21,7 @@ def execute_command(data) logger.error "Received unrecognized command in #{data.inspect}" end rescue Exception => e + @connection.rescue_with_handler(e) logger.error "Could not execute command from (#{data.inspect}) [#{e.class} - #{e.message}]: #{e.backtrace.first(5).join(" | ")}" end diff --git a/actioncable/test/connection/subscriptions_test.rb b/actioncable/test/connection/subscriptions_test.rb index 902085c5d6..1da2f78179 100644 --- a/actioncable/test/connection/subscriptions_test.rb +++ b/actioncable/test/connection/subscriptions_test.rb @@ -3,12 +3,25 @@ require "test_helper" class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase + class ChatChannelError < Exception; end + class Connection < ActionCable::Connection::Base - attr_reader :websocket + attr_reader :websocket, :exceptions + + rescue_from ChatChannelError, with: :error_handler + + def initialize(*) + super + @exceptions = [] + end def send_async(method, *args) send method, *args end + + def error_handler(e) + @exceptions << e + end end class ChatChannel < ActionCable::Channel::Base @@ -22,6 +35,10 @@ def subscribed def speak(data) @lines << data end + + def throw_exception(_data) + raise ChatChannelError.new("Uh Oh") + end end setup do @@ -85,6 +102,19 @@ def speak(data) end end + 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 + test "unsubscribe from all" do run_in_eventmachine do setup_connection diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md index 23a43e4cc0..5ebbf1e7e0 100644 --- a/guides/source/action_cable_overview.md +++ b/guides/source/action_cable_overview.md @@ -128,6 +128,27 @@ can use this approach: verified_user = User.find_by(id: cookies.encrypted['_session']['user_id']) ``` +#### Exception Handling + +By default, unhandled exceptions are caught and logged to Rails' logger. If you would like to +globally intercept these exceptions and report them to an external bug tracking service, for +example, you can do so with `rescue_with`. + +```ruby +# app/channels/application_cable/connection.rb +module ApplicationCable + class Connection < ActionCable::Connection::Base + rescue_from StandardError, with: :report_error + + private + + def report_error(e) + SomeExternalBugtrackingService.notify(e) + end + end +end +``` + ### Channels A *channel* encapsulates a logical unit of work, similar to what a controller does in a @@ -175,6 +196,25 @@ class ChatChannel < ApplicationCable::Channel end ``` +#### Exception Handling + +As with `ActionCable::Connection::Base`, you can also use +[`rescue_with`](https://api.rubyonrails.org/classes/ActiveSupport/Rescuable/ClassMethods.html) +on a specific channel to handle raised exceptions: + +```ruby +# app/channels/chat_channel.rb +class ChatChannel < ApplicationCable::Channel + rescue_from 'MyError', with: :deliver_error_message + + private + + def deliver_error_message(e) + broadcast_to(...) + end +end +``` + ## Client-Side Components ### Connections -- GitLab