Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

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

Open
SteveAlexander opened this issue Dec 6, 2018 · 10 comments
Open

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

SteveAlexander opened this issue Dec 6, 2018 · 10 comments

Comments

@SteveAlexander
Copy link

Copied from flutter/flutter#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)
dart-lang/http#1604      _invokeErrorHandler (dart:async/async_error.dart:13:29)
dart-lang/web_socket_channel#2      _HandleErrorStream._handleError (dart:async/stream_pipe.dart:286:9)
dart-lang/web_socket_channel#3      _ForwardingStreamSubscription._handleError (dart:async/stream_pipe.dart:168:13)
dart-lang/http#1605      _rootRunBinary (dart:async/zone.dart:1144:38)
dart-lang/web_socket_channel#5      _CustomZone.runBinary (dart:async/zone.dart:1037:19)
dart-lang/web_socket_channel#6      _CustomZone.runBinaryGuarded (dart:async/zone.dart:939:7)
dart-lang/web_socket_channel#7      _BufferingStreamSubscription._sendError.sendError (dart:async/stream_impl.dart:355:15)
dart-lang/web_socket_channel#8      _BufferingStreamSubscription._sendError (dart:async/stream_impl.dart:373:16)
dart-lang/web_socket_channel#9      _BufferingStreamSubscription._addError (dart:async/stream_impl.dart:272:7)
dart-lang/web_socket_channel#10     _SyncStreamController._sendError (dart:async/stream_controller.dart:767:19)
dart-lang/web_socket_channel#11     _StreamController._addError (dart:async/stream_controller.dart:647:7)
dart-lang/web_socket_channel#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
Copy link

sintrb commented Dec 23, 2018

Hi, did you found some solution?

@SteveAlexander
Copy link
Author

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
Copy link

sintrb commented Dec 30, 2018

I got it. Thanks.

@rgr-dev
Copy link

rgr-dev commented Jan 29, 2019

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
Copy link

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
Copy link

adityabansalx commented Feb 5, 2023

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
Copy link

Jalmoud2 commented Feb 9, 2023

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
Copy link

adityabansalx commented Feb 10, 2023

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
Copy link

adityabansalx commented Feb 10, 2023

@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
Copy link

skillastat commented Dec 18, 2023

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

  • in a try/catch block, await for channel.ready
  • if we good, we continue to another function that setups the channel.listen
  • otherwise, we try to reconnects. (In my case, I need to retry indefinitely, since my App relyes heavily on WebSockets.)
  • step 4 : ???
  • step 5 : 100% works all the time

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");
    });
    
  }
}

# for free to join this conversation on GitHub. Already have an account? # to comment
Projects
None yet
Development

No branches or pull requests

8 participants