Skip to content

Rescue subscription errors instead of killing WebSocket connection#4

Closed
bradgessler wants to merge 2 commits into
socketry:mainfrom
bradgessler:fix-subscription-error-handling
Closed

Rescue subscription errors instead of killing WebSocket connection#4
bradgessler wants to merge 2 commits into
socketry:mainfrom
bradgessler:fix-subscription-error-handling

Conversation

@bradgessler
Copy link
Copy Markdown

@bradgessler bradgessler commented Feb 12, 2026

Problem

ActionCable::Connection::Subscriptions::Error (specifically AlreadySubscribedError) can be raised when a client sends a duplicate subscribe command. This is common during Turbo morph/page refresh cycles, where the <turbo-cable-stream-source> element is re-inserted into the DOM and triggers a re-subscribe.

Currently, this exception propagates to the generic rescue => error clause in handle_incoming_websocket, which logs "Abnormal client failure!" and tears down the entire WebSocket connection in the ensure block.

This is especially problematic with the PostgreSQL subscription adapter, where connection teardown removes all LISTEN subscriptions. With rapid reconnect/resubscribe cycles, the LISTEN thread never stays alive long enough to receive NOTIFY broadcasts from other processes (e.g., background job workers), effectively breaking cross-process ActionCable broadcasts.

Fix

Wrap connection.handle_incoming in a targeted rescue ActionCable::Connection::Subscriptions::Error so subscription-level errors are logged as warnings but the WebSocket connection stays alive.

Testing

Observed in production at https://og.plus with:

  • Falcon + async-cable + actioncable-next + PostgreSQL adapter
  • Separate web and worker processes (Fly.io)
  • Turbo page refreshes via broadcasts_refreshes

Before the fix: zero LISTEN connections in pg_stat_activity, broadcasts silently lost.
After the fix: WebSocket connections survive duplicate subscribe attempts, LISTEN stays active, broadcasts delivered.

@bradgessler bradgessler marked this pull request as ready for review February 12, 2026 20:47
bradgessler and others added 2 commits May 29, 2026 19:00
ActionCable::Connection::Subscriptions::Error (e.g. AlreadySubscribedError)
can be raised when a client sends a duplicate subscribe command, which is
common during Turbo morph/page refresh cycles. Previously this propagated
to the generic rescue clause, tearing down the entire WebSocket connection.

This is especially problematic with the PostgreSQL subscription adapter,
where connection teardown removes all LISTEN subscriptions. With rapid
reconnect/resubscribe cycles, the LISTEN thread never stays alive long
enough to receive NOTIFY broadcasts from other processes.

The fix wraps connection.handle_incoming in a targeted rescue so
subscription errors are logged but the connection stays alive.
@samuel-williams-shopify samuel-williams-shopify force-pushed the fix-subscription-error-handling branch 2 times, most recently from 5e15d3a to c5826db Compare May 29, 2026 10:23
@samuel-williams-shopify
Copy link
Copy Markdown
Contributor

@palkan

rails/rails@7a8f26dcff changed this behaviour to raise an exception, but it seems like some adapters legitimately raise this error in some cases. WDYT?

@samuel-williams-shopify
Copy link
Copy Markdown
Contributor

I think this is better solved upstream, e.g. rails/rails#57504

@palkan
Copy link
Copy Markdown

palkan commented May 29, 2026

@bradgessler As a quick workaround, you can add rescue_from AlreadySubscribedError to your base Connection class.

This is common during Turbo morph/page refresh cycles, where the element is re-inserted into the DOM and triggers a re-subscribe.

Or switch to @anycable/web and forget about that 🙂.

@samuel-williams-shopify In general, I think, it worths adding exception handling to the message processing loop (like Rails does that). Maybe, not Exception, but just StandardError. For example, we don't want to drop connection because of ActiveRecord::NotFoundError, right? (We lack a proper mechanism to communicate errors with clients 🤷‍♂️)

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

Successfully merging this pull request may close these issues.

3 participants