Testing WebSocket connections - flutter

I'm using WebSocket from dart:io and IOWebSocketChannel (https://pub.dev/packages/web_socket_channel) to create a connection with channel subscriptions.
The code is largely working, I can connect to my server and subscribe to channels, receive data etc, but I'm struggling with an approach to writing tests.
Given this constructor:
SocketServer.Connect(this.url, {this.onConnect}) {
channel = IOWebSocketChannel.connect(url);
_stream = channel.stream.listen((data) {
this.onConnect();
final payload = jsonDecode(data);
print('Received: ');
print(payload);
}, onError: (_) {
this.disconnect();
});
}
I've tried variations on the following:
test("it should create a connection", () async {
final server = await HttpServer.bind('localhost', 0);
server.transform(WebSocketTransformer()).listen((WebSocket socket) async {
final channel = await IOWebSocketChannel(socket);
channel.sink.add('test');
});
bool connected = false;
final websocketServer = SocketServer.Connect('ws://localhost:${server.port}/cable', onConnect: (SocketServer connection) {
print('connected');
connected = true;
});
expectLater(connected, true);
});
But I don't receive the 'connected' output, since my HttpServer isn't sending back the correct responses.
Would it be best to depend on a real server to handle the connections, or create one in the tests?

Related

how to handle a loop in flutter? need can be canceled?

I want to use flutter fulfilled a mqtt client. the client need send message to serve for loop.
I used "while" keyword for loop. but the flutter UI is pending during the while function.
if use isolate to work it, sending the cancel flag failed.
Does there anybody have the experience on it?
----------code------
onpressed -----> Future Function ----> use while loop---->the app pending cannot press anywhere---> the loop stop, the app recovery
I want start a mqtt client to send mqtt message for loop.
but when pressed the function, the while loop pended the app.
Expected results: the loop works in an async work, and the app can work rightly.
Actual results: the app is pending.
###UI
children: [
SizedBox(width: 200),
MaterialButton(
child: Text('发送'),
onPressed: () {
BtnsendMsg();
},
####onpressed function
mc = new MqttClient(false);
BtnsendMsg() async {
mc.MsgSend(clientid, topic, msgname, '3');
print("back");
}
####loop function
class MqttClient {
bool isStop;
MqttClient(this.isStop);
Future MsgSend(clientid, topic, msgname, interval) async {
isStop = false;
var cc = await clientGet(clientid);
var msg = await msgGet(msgname);
String host = "1.1.1.1";
String msgdata = "1111";
if (cc != null) {
host = cc.host!;
}
if (msg != null) {
msgdata = msg.msgdata!;
}
Future future = Future(() {
while (isStop == false) {
SendMsgOnce(host, clientid, topic, msgdata);
sleep(Duration(seconds: 3));
}
});
sleep(Duration(seconds: 30));
isStop = true;
}
This is because you are putting load on main thread by not using async and await while sending request to the server. Do the following changes to your code then it should get work.
class MqttClient {
bool isStop;
MqttClient(this.isStop);
Future MsgSend(clientid, topic, msgname, interval) async {
isStop = false;
var cc = await clientGet(clientid);
var msg = await msgGet(msgname);
String host = "1.1.1.1";
String msgdata = "1111";
if (cc != null) {
host = cc.host!;
}
if (msg != null) {
msgdata = msg.msgdata!;
}
Future future = Future(() async {
while (isStop == false) {
await SendMsgOnce(host, clientid, topic, msgdata);
sleep(Duration(seconds: 3));
}
});
sleep(Duration(seconds: 30));
isStop = true;
}
In your on pressed function you are using async but now awaiting for that
mc = new MqttClient(false);
BtnsendMsg() async {
await mc.MsgSend(clientid, topic, msgname, '3');
print("back");
}

Flutter handling http timeout - closing connection

I am having hard times with figuring out how to handle exceptions and closed connections using Flutter http package https://pub.dev/packages/http.
I have setup timeout of 10 sec and want to show some response to a user within that 10 sec. There is CircularProgreseIndicator and activated by isLoading variable, with error text wrapped up in UI for better presentation.
This example below will timeout after 10 seconds, but after around 20-30 sec, app will crash with an error Connection closed before full header was received
Same error will be thrown if I use client.close() (including finally section).
I would like to control timeout time (4-5 sec) and close connection if no response is received within 5 sec. I don't want to implement automatic retry (want to use pull to refresh/retry manually etc.)
Would anyone know how to implement client.close() properly? Thx
Example:
Future<String> getData() async {
final client = http.Client();
try {
var response = await client.get(Uri.parse(endpoint)).timeout(
Duration(seconds: 10),
onTimeout: () {
// Closing client here throwns an error
// client.close(); // Connection closed before full header was received
return http.Response('Error', 500); // Replace 500 with your http code.
},
);
// Some logic for state etc.
if (response.statusCode == 200) {
this.setState(() {
data = jsonDecode(response.body);
isLoading = false;
isError = false;
});
return "success";
} else {
// Closing client here throwns an error
// client.close(); // Connection closed before full header was received
setState(() {
isLoading = false;
isError = true;
});
return "error";
}
// Finally... close connection
} finally {
// Closing client here throwns an error
// client.close(); // Connection closed before full header was received
}
}
UPDATE: (workaround)
I have found a bit different approach and app does not crash with example below. Loading will be shown all the time if there is no communication with the server (no internet connection). If server returns anything but 200 (ok), "Server error occurred" will be shown.
Future<String> getData() async {
String category = await getCategory();
String endpoint =
Endpoints().getEndpoint("apiServer") + '/categories/' + category;
final client = RetryClient(http.Client());
try {
var response = await client.get(Uri.parse(endpoint));
// Added extra loading time for better UI
await Future.delayed(const Duration(milliseconds: 700), () {});
if (response.statusCode == 200) {
this.setState(() {
data = jsonDecode(response.body);
isLoading = false;
});
showSnackbar("success", "Category list retreived");
return "success";
} else {
// client.close(); // Connection closed before full header was received
setState(() {
isLoading = false;
isError = true;
});
showSnackbar("error", "Server error occurred. Try again");
return "error";
}
} finally {
client.close();
}
}

websocket automatic reconnect with flutter & riverpod?

1. OBJECTIVE
I would like the connection between my custom WebSocket server (API) and my Flutter app, to be re-established automatically when encountering network issues or when the WebSocket server encounter issues.
Use case 1: the wifi stops and suddenly comes back.
Use case 2: the API is not started and restarts suddenly.
Constraint: I use Riverpod as a state management library (and I want to keep it :)).
I emphasize about the state management library because I create the WS connection in a StreamProvider (cf. Riverpod).
2. INITIAL SETUP WITHOUT AUTOMATIC RECONNECT
I created a StreamProvider as shown below:
final hostProvider =
StreamProvider.autoDispose.family<Host, String>((ref, ip) async* {
//SOCKET OPEN
final channel = IOWebSocketChannel.connect('ws://$ip:$port/v1/path');
ref.onDispose(() {
// SOCKET CLOSE
return channel.sink.close();
});
await for (final json in channel.stream) {
final jsonStr = jsonDecode(json as String);
yield Host.fromJson(jsonStr as Map<String, dynamic>);
}
});
And I created a widget to consume the data:
useProvider(hostProvider(ip)).when(
data: (data) => show the result
loading: () => show progress bar
error: (error, _) => show error
);
This piece of code works great. However, there is no automatic reconnect mechanism.
3. AUTOMATIC RECONNECT ATTEMPTS
I called a function connectWs in a try/catch whenever exceptions are caught:
final hostProvider =
StreamProvider.autoDispose.family<Host, String>((ref, ip) async* {
// Open the connection
connectWs('ws://$ip:$port/v1/path').then((value) async* {
final channel = IOWebSocketChannel(value);
ref.onDispose(() {
return channel.sink.close();
});
await for (final json in channel.stream) {
final jsonStr = jsonDecode(json as String);
yield Host.fromJson(jsonStr as Map<String, dynamic>);
}
});
});
Future<WebSocket> connectWs(String path) async {
try {
return await WebSocket.connect(path);
} catch (e) {
print("Error! " + e.toString());
await Future.delayed(Duration(milliseconds: 2000));
return await connectWs(path);
}
}
I created a connectProvider provider, as shown here below, I 'watched' in hostProvider in order to create a channel. Whenever there is an exception, I use the refresh function from the Riverpod library to recreate the channel:
// used in hostProvider
ref.container.refresh(connectProvider(ip))
final connectProvider =
Provider.family<Host, String>((ref, ip) {
//SOCKET OPEN
return IOWebSocketChannel.connect('ws://$ip:$port/v1/path');
});
Thanks in advance for your help.
Thanks, #Dewey.
In the end, I found a workaround that works for my use case:
My providers: channelProvider & streamProvider
static final channelProvider = Provider.autoDispose
.family<IOWebSocketChannel, HttpParam>((ref, httpParam) {
log.i('channelProvider | Metrics - $httpParam');
return IOWebSocketChannel.connect(
'ws://${httpParam.ip}:$port/v1/${httpParam.path}');
});
static final streamProvider =
StreamProvider.autoDispose.family<dynamic, HttpParam>((ref, httpParam) {
log.i('streamProvider | Metrics - $httpParam');
log.i('streamProvider | Metrics - socket ${httpParam.path} opened');
var bStream = ref
.watch(channelProvider(httpParam))
.stream
.asBroadcastStream(onCancel: (sub) => sub.cancel());
var isSubControlError = false;
final sub = bStream.listen(
(data) {
ref
.watch(channelProvider(httpParam))
.sink
?.add('> sink add ${httpParam.path}');
},
onError: (_, stack) => null,
onDone: () async {
isSubControlError = true;
await Future.delayed(Duration(seconds: 10));
ref.container.refresh(channelProvider(httpParam));
},
);
ref.onDispose(() {
log.i('streamProvider | Metrics - socket ${httpParam.path} closed');
sub.cancel();
if (isSubControlError == false)
ref.watch(channelProvider(httpParam)).sink?.close(1001);
bStream = null;
});
return bStream;
});
I consume streamProvider that way in my widget:
return useProvider(MetricsWsRepository.streamProvider(HttpParam(
ip: ip,
path: 'dummy-path',
))).when(
data: (data) => deserialize & doSomething1,
loading:() => doSomething2,
error: (_, stack) => doSomething3
)
I'm a bit of a beginner with riverpod but it seems to me you want to use a higher-level redux/bloc style flow to recreate the provider each time it fails ...
This higher level bloc creates the provider when the connection succeeds, and when the connection fails, you dispatch an event to the bloc that tells it to reconnect and recreate the provider ...
That's my thought, but again, I'm a beginner with this package.

Trouble with WebSockets in Flutter

I am having some trouble implementing WebSockets in my flutter application.
Here is code my code:
void connectToWebSocket() {
print("trying to connect to websocket");
final Future futureChannel = establishConnection();
futureChannel.then((future) {
print("Connection established, registering interest now...");
channel = future;
webSocketConnected = true;
channel.sink.add({
"action": "saveConnection",
"UserName": "rakshak#gmail.com",
"DeviceId": "1d0032000947363339343638"
});
}).catchError((error) {
channel = null;
webSocketConnected = false;
webSocketConnectionError = error.toString();
print("Connection failed \n $webSocketConnectionError");
});
}
Future<IOWebSocketChannel> establishConnection() async {
final IOWebSocketChannel channel = IOWebSocketChannel.connect(
'wss://1j839fy6t3.execute-api.us-east-1.amazonaws.com/Dev');
return channel;
}
Nothing seems to happen when this code runs. I can see the print messages saying "trying to connect to WebSocket" and "Connection established, registering interest now..." on the console.
The WebSocket is implemented using AWS API Gateway and I can see in the logs that the Flutter app has not connected to the WebSocket.
I have tested the WebSocket using wscat command-line tool and I know that it works.
I am not seeing any error in the console.
Let me know if you would like to see any more of my code.
Turns out you channel.sink.add accepts a string and not a Map.
Replace
channel.sink.add({
"action": "saveConnection",
"UserName": "rakshak#gmail.com",
"DeviceId": "1d0032000947363339343638"
});
With
channel.sink.add('{
"action": "saveConnection",
"UserName": "rakshak#gmail.com",
"DeviceId": "1d0032000947363339343638"
}');
and it should work.
I don't understand what you want. But If you want websocket, you can refer below one.
Add Dart Pub
adhara_socket_io
Add class
class SignalServer {
static SignalServer _instatnce;
factory SignalServer() => _instatnce ?? new SignalServer._();
SignalServer._();
SocketIO socketIO;
int State = 0;
void ConnectServer() async {
this.socketIO = await SocketIOManager().createInstance("http://192.168.50.65:8081");
socketIO.onConnect((data) {
print("Signal server connected");
State = 1;
});
socketIO.onDisconnect((_) {
print("Signal Disconnected");
State = 0;
});
socketIO.connect();
}
}
For Instance(main.dart)
static SignalServer signalServer;
....
signalServer = new SignalServer();
signalServer.ConnectServer();
For use
In any widget
void initState() {
super.initState();
setSocketEvent();
}
void setSocketEvent() async{
await MyApp.signalServer.socketIO.on("room-ready", (data) {
//enter your code
});
await MyApp.signalServer.socketIO.on("on-message", (data) {
//enter your code
});
...
}
I hope it will help you.

Retry Http Get request if there is no response in Flutter

getData() async {
http.Response response = await http.get('https://www.example.com/);
print(response.body);
}
The above function works to get the HTML code of a page but it fails in some cases. The function is sometimes never completed and it waits forever to get response( For example, if the app is opened while internet is off and even when its turned on, it never connects). In such situations is there any way to retry ?
I tried the http retry package but it gives me 15+ errors.
Example code for how this could be done:
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<List> loadData() async {
bool loadRemoteDatatSucceed = false;
var data;
try {
http.Response response = await http.post("https://www.example.com",
body: <String, String>{"username": "test"});
data = json.decode(response.body);
if (data.containsKey("success")) {
loadRemoteDatatSucceed = true;
}
} catch (e) {
if (loadRemoteDatatSucceed == false) retryFuture(loadData, 2000);
}
return data;
}
retryFuture(future, delay) {
Future.delayed(Duration(milliseconds: delay), () {
future();
});
}
You can use RetryPolicy from http package to retry your connection, just create your own class and inherit form RetryPolicy and override these function like the following example, then create a Client using HttpClientWithInterceptor.build and add your custom retryPolicy as a parameter, this will retry your request for a number of times until a condition is met, if not, it'll just stop retrying.
import 'package:http/http.dart';
class MyRetryPolicy extends RetryPolicy {
final url = 'https://www.example.com/';
#override
// how many times you want to retry your request.
int maxRetryAttempts = 5;
#override
Future<bool> shouldAttemptRetryOnResponse(ResponseData response) async {
//You can check if you got your response after certain timeout,
//or if you want to retry your request based on the status code,
//usually this is used for refreshing your expired token but you can check for what ever you want
//your should write a condition here so it won't execute this code on every request
//for example if(response == null)
// a very basic solution is that you can check
// for internet connection, for example
try {
final result = await InternetAddress.lookup('google.com');
if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) {
return true;
}
return false;
} on SocketException catch (_) {
return false;
}
}
}
then create and use a client to make your requests.
it will automatically retry the request if the condition you wrote is met.
Client client = HttpClientWithInterceptor.build(
retryPolicy: ExpiredTokenRetryPolicy(),
);
final response = await client.get('https://www.example.com/);
there is also a package to check for internet connection if that your problem, see connectivity
You can use try-catch blocks inside async functions like you would in synchronous code. Perhaps you'd be able to add some sort of error handling mechanism in the function, and retry the function on error? Here's some documentation on that one.
Example from the docs:
try {
var order = await getUserOrder();
print('Awaiting user order...');
} catch (err) {
print('Caught error: $err');
}
You can also catch specific Exceptions, per this github issue.
doLogin(String username, String password) async {
try {
var user = await api.login(username, password);
_view.onLoginSuccess(user);
} on Exception catch(error) {
_view.onLoginError(error.toString());
}
}
EDIT: This may also help.
While we're at it, look here for a function that reattempts an async operation however many times you need.