Skip to content

fix(network): RawWebSocketClientImpl.connect never awaits channel.ready - isConnected is true for a dead socket and the failure escapes unhandled #262

Description

@koniz-dev

Context

RawWebSocketClientImpl (lib/core/network/realtime/raw_websocket_client.dart,
untouched since its introducing commit 302490b) is the implementation the
provider doc tells adopters to inject for production
(lib/core/network/realtime/realtime_providers.dart:6-7: "Override this
provider to inject RawWebSocketClientImpl for production").

connect() (raw_websocket_client.dart:21-45) assigns
WebSocketChannel.connect(...) and returns. WebSocketChannel.connect
never throws for a network failure - the failure is reported through the
channel's ready future, which nothing awaits or handles. Two consequences,
both verified by running the class against a port that refuses connections:

$ dart --packages=.dart_tool/package_config.json scratchpad/ws_probe.dart
connect() returned normally: true
isConnected right after connect: true
Unhandled exception:
WebSocketChannelException: SocketException: Connection refused (OS Error: Connection refused, errno = 61), address = 127.0.0.1, port = 52118

(probe: await client.connect('ws://127.0.0.1:1'); print(client.isConnected);)

  1. connect() reports success and isConnected reports true for a
    connection that never opened
    (isConnected is just _channel != null,
    :18). Calls to send() are then silently buffered into a dead sink.
  2. The connection failure escapes as an unhandled async exception from
    the unawaited ready future. The try/catch around
    WebSocketChannel.connect at :28-44 can never catch it (the
    constructor does not throw for network errors), and the stream onError
    handler at :31-35 does not cover the ready future.

The existing test file
(test/core/network/realtime/raw_websocket_client_test.dart) never
exercises a connection attempt, so nothing guards this. An adopter who
follows the provider doc inherits a realtime client whose failure mode on an
unreachable server is a lying isConnected plus an unhandled exception.

No prior issue covers this surface (gh issue list --state all --search "websocket" and --search "realtime" return nothing about it).

Acceptance criteria

All driveable with flutter test on the host VM using dart:io sockets -
no device, no backend.

  1. A test that calls connect() against a port that refuses connections
    (bind a ServerSocket on port 0, note the port, close it, connect)
    observes a defined, documented failure: either connect() completes
    with an error, or the error is delivered on stream - and in both cases
    no unhandled async error escapes (assert via runZonedGuarded or by the
    test simply not failing on an uncaught exception).
  2. After that failed connect, isConnected is false.
  3. A companion happy-path test using an in-process server
    (HttpServer + WebSocketTransformer) shows connect() completing,
    isConnected true, a send() arriving at the server, and messages
    from the server arriving on stream - proving the fix did not break the
    working path.
  4. IRealtimeClient.connect's doc comment
    (lib/core/network/realtime/i_realtime_client.dart) states the error
    contract implementations must meet.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    epic:core-networklib/core/network: Dio ApiClient, interceptors, realtime/WebSocketpriority:P2Normal queue: real but not blockingtype:bugSomething that is broken relative to documented behavior

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions