dart-lang / web_socket_channel

StreamChannel wrappers for WebSockets.
https://pub.dev/packages/web_socket_channel
BSD 3-Clause "New" or "Revised" License
423 stars 112 forks source link

some errors can't be handled in IOWebSocketChannel.connect #38

Open SteveAlexander opened 5 years ago

SteveAlexander commented 5 years ago

Copied from https://github.com/flutter/flutter/issues/21076

IOWebSocketChannel.connect suffers from the problem described here:

https://www.dartlang.org/guides/libraries/futures-error-handling#potential-problem-failing-to-register-error-handlers-early

When I use connect with a URL that can't be resolved (e.g. by having my wifi turned off), then I get an exception that can't be caught by channel.stream.handleError(onError).

All exceptions should be handleable by stream.handleError, particularly as this is what the docs promise: "If there's an error connecting, the channel's stream emits a WebSocketChannelException wrapping that error and then closes."

ref: https://docs.flutter.io/flutter/web_socket_channel.io/IOWebSocketChannel/IOWebSocketChannel.connect.html

flutter: #0      new IOWebSocketChannel._withoutSocket.<anonymous closure> (package:web_socket_channel/io.dart:83:24)
#1      _invokeErrorHandler (dart:async/async_error.dart:13:29)
#2      _HandleErrorStream._handleError (dart:async/stream_pipe.dart:286:9)
#3      _ForwardingStreamSubscription._handleError (dart:async/stream_pipe.dart:168:13)
#4      _rootRunBinary (dart:async/zone.dart:1144:38)
#5      _CustomZone.runBinary (dart:async/zone.dart:1037:19)
#6      _CustomZone.runBinaryGuarded (dart:async/zone.dart:939:7)
#7      _BufferingStreamSubscription._sendError.sendError (dart:async/stream_impl.dart:355:15)
#8      _BufferingStreamSubscription._sendError (dart:async/stream_impl.dart:373:16)
#9      _BufferingStreamSubscription._addError (dart:async/stream_impl.dart:272:7)
#10     _SyncStreamController._sendError (dart:async/stream_controller.dart:767:19)
#11     _StreamController._addError (dart:async/stream_controller.dart:647:7)
#12     _rootRunBinary (dart:async/zone.dart:1144:38)
flutter doctor -v
[✓] Flutter (Channel master, v0.7.1-pre.26, on Mac OS X 10.13.6 17G2208, locale en-GB)
    • Flutter version 0.7.1-pre.26 at /Users/steve/code/flutter
    • Framework revision 510c0eeaff (3 days ago), 2018-08-24 17:19:30 -0700
    • Engine revision 0914926014
    • Dart version 2.1.0-dev.1.0.flutter-ccb16f7282

[✓] Android toolchain - develop for Android devices (Android SDK 28.0.1)
    • Android SDK at /Users/steve/Library/Android/sdk
    • Android NDK location not configured (optional; useful for native profiling support)
    • Platform android-28, build-tools 28.0.1
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1024-b01)
    • All Android licenses accepted.

[✓] iOS toolchain - develop for iOS devices (Xcode 9.4.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 9.4.1, Build version 9F2000
    • ios-deploy 1.9.2
    • CocoaPods version 1.5.3

[✓] Android Studio (version 3.1)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin version 26.0.1
    • Dart plugin version 173.4700
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1024-b01)

[✓] VS Code (version 1.26.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 2.17.1

[✓] Connected devices (1 available)
    • iPhone X • 1E0393AA-EF50-42AA-A244-3279599BE2C1 • ios • iOS 11.4 (simulator)

• No issues found!
sintrb commented 5 years ago

Hi, did you found some solution?

SteveAlexander commented 5 years ago

yes, my workaround is to create a WebSocket directly. Something like:

      final socket = await WebSocket
          .connect(url.toString())
          .timeout(_webSocketConnectionTimeout);
      return IOWebSocketChannel(socket);

Then I wrap this in a try-catch, so I can catch SocketException and TimeoutException and handle these in a way that makes sense for my app.

sintrb commented 5 years ago

I got it. Thanks.

rgr-dev commented 5 years ago

I have the same problem, but I thought that the exception should be handled in socketClient.stream.listen( method), this method params receives an Error handler, so i decided put one like this:

socketClient.stream.listen(
    (message){
        // handling incoming messages
    },
    onError: //Here a put a Error handling function,
    onDone: function(){
        // communication has been closed 
    }
);

But nothing happends.

markflarup commented 5 years ago

I have the same problem, but I thought that the exception should be handled in socketClient.stream.listen( method), this method params receives an Error handler, so i decided put one like this:

socketClient.stream.listen(
  (message){
      // handling incoming messages
  },
  onError: //Here a put a Error handling function,
  onDone: function(){
      // communication has been closed 
  }
);

But nothing happends.

@roger357, the following way works for me:

      stream = widget.webSocketChannel.stream;
      streamSubscription = stream.listen(
          onData,
          onError: (error) {
             // method calls and what not here
          },
          cancelOnError: true);
    }

I don't know if it is working because I have a StreamSubscription or whether it simply is an additional step.

adityabansalx commented 1 year ago

I was facing same issue turned out, unhandled Socket execption was thrown during connect method. not by the stream.

I believe IOWenSocketChannel would have similar approach to await and catch error

WebSocketChannel channel = WebSocketChannel.connect(uri );
  try {
    await channel.ready;
  } catch (e) {
   // handle exception here
   print("WebsocketChannel was unable to establishconnection");
  }

Stream stream = channel.stream;
    stream.listen((event) {
      print('Event from Stream: $event');

    },onError: (e){

    // handle stream error
    },
    onDone: (() {
     // stream on done callback... 
    }),
    cancelOnError: true
    );
Jalmoud2 commented 1 year ago

While @adityabansalx solution works, I find it weird that adding await channel.ready makes it catch the error. Can someone please explain this behavior? 🤔

adityabansalx commented 1 year ago

Hey @Jalmoud2, what I understood is, calling connect gives a channel immediately, and initiates the connection to the socket server which may fail. e.g. request timeout. compare it to async Http request. You can await channel.ready to succeed before calling for a stream. In the answer I have called stream even if channel fails, so stream calls onError if connection didnt succeed, this helps me put error handling in one place.. I guess its better to not do so..

The point is channel connection and stream are different things. you can get a Sink or a Stream from the channel and they can have errors during their own functioning

adityabansalx commented 1 year ago

@Jalmoud2, I think putting it this way, would help understand the behavior.

WebSocketChannel channel = WebSocketChannel.connect(Uri());

  channel.ready.then((_) {
    channel.stream.listen((event) {
      print('Event from Stream: $event');

    },onError: (e){

    // handle stream error
    },
    onDone: (() {
     // stream on done callback... 
    }),
    cancelOnError: true
    );

  }).onError((error, stackTrace) {
    print("WebsocketChannel was unable to establishconnection");
  });
skillastat commented 10 months ago

Thanks to you guys comments, I found what I was looking for and here it is:

EDIT: Added reconnect duration each time it failed, until a maximum of 60 seconds (import 'dart:math' as math;)... Reset the duration to 3 on connection succes.

// Flag to indicate whether a WebSocket connection attempt is currently in progress.
// This is used to prevent multiple simultaneous attempts to connect to the WebSocket, 
// which could lead to unexpected behavior or resource issues.

bool websocketConnecting = false;

// Reconnect duration starts at 3 seconds
int reconnectDuration = 3;

// This function is responsible for establishing a connection to a WebSocket server.
void connectWebsockets() async {

  // Check if a connection attempt is already in progress. If so, exit the function
  // early to prevent another concurrent attempt. This is important for maintaining
  // control over the connection process and avoiding unnecessary network traffic.

  if (websocketConnecting) {
    return; 
  }

  // Set the flag to true, indicating that a connection attempt is now underway.

  websocketConnecting = true;
  log("Attempting to connect to WebSocket");

  // Parse the WebSocket URL from a predefined constant. This URL is where the client
  // will attempt to establish the WebSocket connection.

  final wsUrl = Uri.parse(ApiConstants.WSOCKET);

  // Connect to the WebSocket server at the specified URL.
  // The pingInterval is set to send a "ping" message every 15 seconds to keep the 
  // connection alive and check its health. The connectTimeout is set to 20 seconds,
  // meaning the connection attempt will time out if not established within this duration.

  channel = IOWebSocketChannel.connect(
    wsUrl,
    pingInterval: Duration(seconds: 15),
    connectTimeout: Duration(seconds: 20)
  );

  try {

    // Await the ready property of the channel, which is a Future that completes when
    // the WebSocket connection is successfully established. This ensures that the 
    // following code only runs after a successful connection.

    await channel!.ready;
    print('WebSocket channel is ready');

    // Once the connection is established, set up the stream listeners to handle 
    // incoming messages, errors, and connection closures.

    setupWebSocketListeners();

  } catch (e) {

    // If an error occurs during the connection attempt or while waiting for the
    // connection to become ready, log the error and perform cleanup.

    print('WebSocket connection failed or stream error occurred: $e');

    // Set the channel to null to clean up and indicate that there is no active 
    // WebSocket connection.

    channel = null;

    // Reset the websocketConnecting flag to allow future connection attempts.

    websocketConnecting = false;

    // Retry connecting to the WebSocket. This creates resilience in the face of 
    // network issues or temporary server unavailability.

    Future.delayed(Duration(seconds: reconnectDuration), () {
      connectWebsockets();
      // Increment reconnectDuration by 3, up to a maximum of 60 seconds
      reconnectDuration = math.min(reconnectDuration + 3, 60);
      print("reconnectDuration: $reconnectDuration");
    });

  }
}