Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

gRPC in Flutter crash when no internet

I'm developing a Flutter app using gRPC and everything was working correctly until I decided to see what happens if there is no internet connection.

After doing that and making a request I get following error:

E/flutter (26480): gRPC Error (14, Error making call: Bad state: The http/2 connection is no longer active and can therefore not be used to make new streams.)

The problem is that even after re-enabling the connection, the error still occurs.
Do I have to recreate the clientChannel?

const String serverUrl = 'theaddress.com';
const int serverPort = 50051;

final ClientChannel defaultClientChannel = ClientChannel(
  serverUrl,
  port: serverPort,
  options: const ChannelOptions(
    credentials: const ChannelCredentials.insecure(),
  ),
);

I would simply like to throw some errors, but work properly once the internet connection comes back.
like image 542
Marcin Szałek Avatar asked Jan 09 '19 07:01

Marcin Szałek


3 Answers

Based on @Ishaan's suggestion, I've used Connectivity package to create a client that reconnects when the internet is back up. So far it seems to be working.

import 'dart:async';

import 'package:connectivity/connectivity.dart';
import 'package:flutter_worker_app/generated/api.pbgrpc.dart';
import 'package:grpc/grpc.dart';
import 'package:rxdart/rxdart.dart';

class ConnectiveClient extends ApiClient {

  final CallOptions _options;
  final Connectivity _connectivity;
  ClientChannel _channel;
  bool hasRecentlyFailed = false;


  ConnectiveClient(this._connectivity, this._channel, {CallOptions options})
      : _options = options ?? CallOptions(),
        super(_channel) {
    //TODO: Cancel connectivity subscription
    _connectivity.onConnectivityChanged.listen((result) {
      if (hasRecentlyFailed && result != ConnectivityResult.none) {
        _restoreChannel();
      }
    });
  }

  ///Create new channel from original channel
  _restoreChannel() {
    _channel = ClientChannel(_channel.host,
        port: _channel.port, options: _channel.options);
    hasRecentlyFailed = false;
  }

  @override
  ClientCall<Q, R> $createCall<Q, R>(
      ClientMethod<Q, R> method, Stream<Q> requests,
      {CallOptions options}) {
    //create call
    BroadcastCall<Q, R> call = createChannelCall(
      method,
      requests,
      _options.mergedWith(options),
    );
    //listen if there was an error
    call.response.listen((_) {}, onError: (Object error) async {
      //Cannot connect - we assume it's internet problem
      if (error is GrpcError && error.code == StatusCode.unavailable) {
        //check connection
        _connectivity.checkConnectivity().then((result) {
          if (result != ConnectivityResult.none) {
            _restoreChannel();
          }
        });
        hasRecentlyFailed = true;
      }
    });
    //return original call
    return call;
  }

  /// Initiates a new RPC on this connection.
  /// This is copy of [ClientChannel.createCall]
  /// The only difference is that it creates [BroadcastCall] instead of [ClientCall]
  ClientCall<Q, R> createChannelCall<Q, R>(
      ClientMethod<Q, R> method, Stream<Q> requests, CallOptions options) {
    final call = new BroadcastCall(method, requests, options);
    _channel.getConnection().then((connection) {
      if (call.isCancelled) return;
      connection.dispatchCall(call);
    }, onError: call.onConnectionError);
    return call;
  }
}

///A ClientCall that can be listened multiple times
class BroadcastCall<Q, R> extends ClientCall<Q, R> {
  ///I wanted to use super.response.asBroadcastStream(), but it didn't work.
  ///I don't know why...
  BehaviorSubject<R> subject = BehaviorSubject<R>();

  BroadcastCall(
      ClientMethod<Q, R> method, Stream<Q> requests, CallOptions options)
      : super(method, requests, options) {
    super.response.listen(
          (data) => subject.add(data),
          onError: (error) => subject.addError(error),
          onDone: () => subject.close(),
        );
  }

  @override
  Stream<R> get response => subject.stream;
}
like image 197
Marcin Szałek Avatar answered Nov 05 '22 17:11

Marcin Szałek


I guess you're one of the few people attempting it.

GRPC connections take a bit of time to create a new connection, not just in dart, but all other languages. If you want, you can put a catch listener on the error code 14 and manually kill the connection and re-connect. There's also idleTimeout channel option that might be of help to you, the default for which is 5 mins in grpc-dart

There was a fix for the unexpted crash issue https://github.com/grpc/grpc-dart/issues/131 , so do try to update your dependencies (grpc-dart) which will prevent the crash, but the problem of reconnection on network might still remain.

After this fix the crashes have stopped, but the stale connection issue does remain for me too. I've resorted to showing snackbars with sentences like "Cannot connect to servers, please try again in a few minutes".

like image 36
ishaan Avatar answered Nov 05 '22 19:11

ishaan


I haven't been using gRPC until today.

Since I've taken time to try to simulate this error I'll post here my answer but all my intel has been lead by @ishann answer that I've upvoted and that should be the accepted one.

I've just tried dart hello world example.

I've the server running on my machine and the client as a Flutter application.

When I don't run the server I get the error

gRPC Error (14, Error connecting: SocketException:

enter image description here

But as soon as the server goes up, all start working as expected, but then I've realized that I was recreating the channel every time, so that's not the OP scenario.

That's my fist Flutter code:

void _foo() async {
  final channel = new ClientChannel('192.168.xxx.xxx',
      port: 50051,
      options: const ChannelOptions(
          credentials: const ChannelCredentials.insecure()));
  final stub = new GreeterClient(channel);

  final name = 'world';

  var _waitHelloMessage = true;
  while (_waitHelloMessage) {
    try {
      final response = await stub.sayHello(new HelloRequest()..name = name);
      print('Greeter client received: ${response.message}');
      _waitHelloMessage = false;
    } catch (e) {
      print('Caught error: $e');
      sleep(Duration(seconds: 1));
    }
  }
  print('exiting');
  await channel.shutdown();
}

Same behaviour if I put device in airplain mode and than switch back to normal wifi/lte connection.

With this other playground project instead I've reproduced either

Caught error: gRPC Error (14, Error making call: Bad state: The http/2 connection is no longer active and can therefore not be used to make new streams.)

From which you cannot come up without recreating the channel, and

Caught error: gRPC Error (14, Error connecting: SocketException: OS Error: Connection refused, errno = 111, address = 192.168.1.58, port = 38120)

(for example shut down the server) from which instead you can get up again without recreating the channel.

The former error code it's not so easy to get because it seems that the channel throttle between wifi and lte connection.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app_test_grpc/grpc/generated/helloworld.pbgrpc.dart';
import 'package:grpc/grpc.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  ClientChannel _channel;

  @override
  void dispose() {
    _shutdown();
    super.dispose();
  }

  void _shutdown() async {
    if (null != _channel) {
      print('shutting down...');
      await _channel.shutdown();
      print('shut down');
      _channel = null;
    } else {
      print ('connect first');
    }
  }

  void _connect() {
    print('connecting...');
    _channel = new ClientChannel('192.168.xxx.xxx',
        port: 50051,
        options: const ChannelOptions(
            credentials: const ChannelCredentials.insecure()));
    print('connected');
  }

  void _sayHello() async {
    if (_channel != null) {
      final stub = new GreeterClient(_channel);

      final name = 'world';

      try {
        final response = await stub.sayHello(new HelloRequest()..name = name);
        print('Greeter client received: ${response.message}');
      } catch (e) {
        print('Caught error: $e');
        //sleep(Duration(seconds: 2));
      }

      //print('exiting');
      //await channel.shutdown();
    } else {
      print('connect first!');
    }
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: Padding(
        padding: const EdgeInsets.only(left: 36.0),
        child: Row(
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: FloatingActionButton(
                onPressed: _connect,
                tooltip: 'Increment',
                child: Icon(Icons.wifi),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: FloatingActionButton(
                onPressed: _sayHello,
                tooltip: 'Increment',
                child: Icon(Icons.send),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: FloatingActionButton(
                onPressed: _shutdown,
                tooltip: 'Increment',
                child: Icon(Icons.close),
              ),
            ),
          ],
        ),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

enter image description here

That's my flutter doctor -v if could be of any help:

$ flutter doctor -v
[✓] Flutter (Channel beta, v1.0.0, on Mac OS X 10.14.1 18B75, locale en-IT)
    • Flutter version 1.0.0 at /Users/shadowsheep/flutter/flutter
    • Framework revision 5391447fae (6 weeks ago), 2018-11-29 19:41:26 -0800
    • Engine revision 7375a0f414
    • Dart version 2.1.0 (build 2.1.0-dev.9.4 f9ebf21297)

[✓] Android toolchain - develop for Android devices (Android SDK 28.0.3)
    • Android SDK at /Users/shadowsheep/Library/Android/sdk
    • Android NDK location not configured (optional; useful for native profiling support)
    • Platform android-28, build-tools 28.0.3
    • 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-1248-b01)
    • All Android licenses accepted.

[✓] iOS toolchain - develop for iOS devices (Xcode 10.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 10.1, Build version 10B61
    • ios-deploy 1.9.4
    • CocoaPods version 1.5.3

[✓] Android Studio (version 3.3)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin version 31.3.3
    • Dart plugin version 182.5124
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1248-b01)

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

[✓] Connected device (1 available)
    [...]

• No issues found!
like image 34
shadowsheep Avatar answered Nov 05 '22 19:11

shadowsheep