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();
}
}
Related
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");
}
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?
I'm trying to get the local IP address in a Flutter Web app. Searching the internet I came around this package: get_ip 0.4.0 - it states it is working under web.
I have this function:
Future<void> initPlatformState() async {
print("Test");
String ipAddress;
// Platform messages may fail, so we use a try/catch PlatformException.
try {
ipAddress = await GetIp.ipAddress;
} on PlatformException {
ipAddress = 'Failed to get ipAddress.';
} on Exception {
print("Exception");
}
print("Ip: $ipAddress");
}
and I call it in initState of main.dart.
The console just has the output Test, it does not output the IP or Exception.
Has someone already used this package? Would there be an other way for a web app to get the local IP?
(I'm using MacOS)
The get_ip package states that it supports Android/iOS.
So,instead of using the package,you can directly interact with ipify endpoint.
Check out their API documentation for more!
Future<String> getIP() async {
try {
const url = 'https://api.ipify.org';
var response = await http.get(url);
if (response.statusCode == 200) {
print(response.body);
return response.body;
} else {
print(response.body);
return null;
}
} catch (exception) {
print(exception);
return null;
}
}
I'm Using ip_geolocation_api Package for getting an IP address and it is working as expected
String text = '';
GeolocationData geolocationData;
#override
void initState() {
super.initState();
this.getIp();
}
Future<void> getIp() async {
geolocationData = await GeolocationAPI.getData();
if (geolocationData != null) {
setState(() {
text = geolocationData.ip;
print(text);
});
}
}
Now You have IP in the text Variable.
This method submits a simple HTTP request and calls a success or error callback just fine:
void _getSimpleReply( String command, callback, errorCallback ) async {
try {
HttpClientRequest request = await _myClient.get( _serverIPAddress, _serverPort, '/' );
HttpClientResponse response = await request.close();
response.transform( utf8.decoder ).listen( (onData) { callback( onData ); } );
} on SocketException catch( e ) {
errorCallback( e.toString() );
}
}
If the server isn't running, the Android-app more or less instantly calls the errorCallback.
On iOS, the errorCallback takes a very long period of time - more than 20 seconds - until any callback gets called.
May I set for HttpClient() a maximum number of seconds to wait for the server side to return a reply - if any?
There are two different ways to configure this behavior in Dart
Set a per request timeout
You can set a timeout on any Future using the Future.timeout method. This will short-circuit after the given duration has elapsed by throwing a TimeoutException.
try {
final request = await client.get(...);
final response = await request.close()
.timeout(const Duration(seconds: 2));
// rest of the code
...
} on TimeoutException catch (_) {
// A timeout occurred.
} on SocketException catch (_) {
// Other exception
}
Set a timeout on HttpClient
You can also set a timeout on the HttpClient itself using HttpClient.connectionTimeout. This will apply to all requests made by the same client, after the timeout was set. When a request exceeds this timeout, a SocketException is thrown.
final client = new HttpClient();
client.connectionTimeout = const Duration(seconds: 5);
You can use timeout
http.get(Uri.parse('url')).timeout(
const Duration(seconds: 1),
onTimeout: () {
// Time has run out, do what you wanted to do.
return http.Response('Error', 408); // Request Timeout response status code
},
);
The HttpClient.connectionTimeout didn't work for me. However, I knew that the Dio packet allows request cancellation. Then, I dig into the packet to find out how they achieve it and I adapted it to me. What I did was to create two futures:
A Future.delayed where I set the duration of the timeout.
The HTTP request.
Then, I passed the two futures to a Future.any which returns the result of the first future to complete and the results of all the other futures are discarded. Therefore, if the timeout future completes first, your connection times out and no response will arrive. You can check it out in the following code:
Future<Response> get(
String url, {
Duration timeout = Duration(seconds: 30),
}) async {
final request = Request('GET', Uri.parse(url))..followRedirects = false;
headers.forEach((key, value) {
request.headers[key] = value;
});
final Completer _completer = Completer();
/// Fake timeout by forcing the request future to complete if the duration
/// ends before the response arrives.
Future.delayed(timeout, () => _completer.complete());
final response = await Response.fromStream(await listenCancelForAsyncTask(
_completer,
Future(() {
return _getClient().send(request);
}),
));
}
Future<T> listenCancelForAsyncTask<T>(
Completer completer,
Future<T> future,
) {
/// Returns the first future of the futures list to complete. Therefore,
/// if the first future is the timeout, the http response will not arrive
/// and it is possible to handle the timeout.
return Future.any([
if (completer != null) completeFuture(completer),
future,
]);
}
Future<T> completeFuture<T>(Completer completer) async {
await completer.future;
throw TimeoutException('TimeoutError');
}
This is an example of how to extend the http.BaseClient class to support timeout and ignore the exception of the S.O. if the client's timeout is reached first.
you just need to override the "send" method...
the timeout should be passed as a parameter to the class constructor.
import 'dart:async';
import 'package:http/http.dart' as http;
// as dart does not support tuples i create an Either class
class _Either<L, R> {
final L? left;
final R? right;
_Either(this.left, this.right);
_Either.Left(L this.left) : right = null;
_Either.Right(R this.right) : left = null;
}
class TimeoutClient extends http.BaseClient {
final http.Client _httpClient;
final Duration timeout;
TimeoutClient(
{http.Client? httpClient, this.timeout = const Duration(seconds: 30)})
: _httpClient = httpClient ?? http.Client();
Future<http.StreamedResponse> send(http.BaseRequest request) async {
// wait for result between two Futures (the one that is reached first) in silent mode (no throw exception)
_Either<http.StreamedResponse, Exception> result = await Future.any([
Future.delayed(
timeout,
() => _Either.Right(
TimeoutException(
'Client connection timeout after ${timeout.inMilliseconds} ms.'),
)),
Future(() async {
try {
return _Either.Left(await _httpClient.send(request));
} on Exception catch (e) {
return _Either.Right(e);
}
})
]);
// this code is reached only for first Future response,
// the second Future is ignorated and does not reach this point
if (result.right != null) {
throw result.right!;
}
return result.left!;
}
}
Their is onError option which works fine if their is any exception like no internet.It has to return response(my case in below code) or null.
In response their are 2 options Body and Status code.
var response = await http.post(url, body: body, headers: _headers).onError(
(error, stackTrace) => http.Response(
jsonEncode({
'message':no internet please connect to internet first
}),
408));
You can also choose to override the settings for a HttpClient:
class DevHttpOverrides extends HttpOverrides {
#override
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
..connectionTimeout = Duration(seconds: 2);
}
}
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.