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

Allow intercepting exceptions that are raised in ActionCable command #34917

Merged
merged 1 commit into from Mar 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions 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*
Expand Down
2 changes: 2 additions & 0 deletions 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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions actioncable/lib/action_cable/connection/subscriptions.rb
Expand Up @@ -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

Expand Down
32 changes: 31 additions & 1 deletion actioncable/test/connection/subscriptions_test.rb
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions guides/source/action_cable_overview.md
Expand Up @@ -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`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we got the method name and the with parameter mixed up:

Suggested change
example, you can do so with `rescue_with`.
example, you can do so with `rescue_from`.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks and sorry!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem!


```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
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here:

Suggested change
[`rescue_with`](https://api.rubyonrails.org/classes/ActiveSupport/Rescuable/ClassMethods.html)
[`rescue_from`](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
Expand Down