Skip to content

fix(realtime_client): detect dropped connections on iOS via WebSocket ping#1451

Merged
spydon merged 3 commits into
mainfrom
fix/realtime-ios-disconnect-detection
Jun 22, 2026
Merged

fix(realtime_client): detect dropped connections on iOS via WebSocket ping#1451
spydon merged 3 commits into
mainfrom
fix/realtime-ios-disconnect-detection

Conversation

@spydon

@spydon spydon commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

What

Enable pingInterval on the native (dart:io) WebSocket so dropped connections are detected consistently across platforms.

Why

Fixes #1071. When the internet drops, Android emits a RealtimeSubscribeException (close code 1002) but iOS emits nothing, leaving the stream silently stale.

Root cause: createWebSocketClient created the native WebSocket without a pingInterval, so a silently dropped connection was never detected at the socket layer:

  • On Android, the OS surfaces the dead TCP connection promptly (RST → close code 1002), so onDone/onError fires and _triggerChanError propagates a channelError.
  • On iOS, the OS buffers writes instead of surfacing a reset. The only fallback was the app-level Phoenix heartbeat, whose recovery path calls conn.sink.close(), which does not reliably fire onDone on a dead iOS socket. So nothing propagated to the channel.

How

Set pingInterval (new Constants.defaultWebSocketPingInterval, 25s, aligned with the existing heartbeat cadence) on the native WebSocket. dart:io now actively probes the peer and closes the connection (goingAway) when a pong is not received, which flows through the same onDone → _onConnClose → _triggerChanError → reconnect path Android already uses.

Scope / safety

  • Only the default native transport changes. Web (websocket_web.dart) is untouched, since browsers manage ping/pong internally.
  • Custom transports injected via RealtimeClientOptions.transport are unaffected.
  • No public API change: the WebSocketTransport typedef is unchanged.
  • A 25s pong window is generous for mobile round-trips.

Testing

  • dart analyze clean on the changed files.
  • Realtime unit suite passes (socket_test, mock_test, channel_test, including the existing CHANNEL_ERROR-on-heartbeat-timeout test).
  • Files formatted.

No unit test added: pingInterval is a property of the underlying dart:io socket and is not observable through IOWebSocketChannel without a live connection, and the codebase does not test the default transport (tests inject mocks).

… ping

The native WebSocket was created without a pingInterval, so a silently
dropped connection was never detected at the socket layer. On Android the
OS surfaces the dead TCP connection promptly (close code 1002), but on iOS
writes are buffered instead of surfacing a reset, so no channelError ever
propagated and the stream went silently stale.

Enable pingInterval on the native WebSocket so it probes the peer and
closes the connection when a pong is not received, flowing through the
same error and reconnect path Android already uses. This makes disconnect
behavior consistent across platforms.

Closes #1071
@spydon spydon requested a review from a team as a code owner June 22, 2026 12:26
Add a regression test using a raw WebSocket server that completes the
handshake but ignores ping frames, asserting the native transport closes
the connection when no pong is received. Expose pingInterval as an
optional, typedef-compatible parameter on createWebSocketClient so the
test can inject a short interval.
@Vinzent03

Copy link
Copy Markdown
Collaborator

Just as a sidenote, I detected some similar issues on web with reconnecting and especially channels rejoining behavior being inefficient. Changes are still local, but should be able to create pr this week.

The capability matrix validator flagged Constants.defaultWebSocketPingInterval
as a new public API symbol. The ping interval is an internal default of the
native transport, so make it a private constant in websocket_io.dart instead of
exposing it on the public Constants class.
@spydon spydon merged commit db754cf into main Jun 22, 2026
27 checks passed
@spydon spydon deleted the fix/realtime-ios-disconnect-detection branch June 22, 2026 17:17
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.

Inconsistent Stream Behavior on Internet Disconnect Between Android and iOS

3 participants