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
#
This commit is contained in:
justin talbott 2020-03-20 13:35:10 -04:00 committed by Jeremy Daer
parent 5df9b4584c
commit d2571e560c
5 changed files with 80 additions and 1 deletions

View File

@ -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*

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true
require "action_dispatch"
require "active_support/rescuable"
module ActionCable
module Connection
@ -46,6 +47,7 @@ module ActionCable
include Identification
include InternalChannel
include Authorization
include ActiveSupport::Rescuable
attr_reader :server, :env, :subscriptions, :logger, :worker_pool, :protocol
delegate :event_loop, :pubsub, to: :server

View File

@ -21,6 +21,7 @@ module ActionCable
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

View File

@ -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 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase
def speak(data)
@lines << data
end
def throw_exception(_data)
raise ChatChannelError.new("Uh Oh")
end
end
setup do
@ -85,6 +102,19 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase
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

View File

@ -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