ferry icon indicating copy to clipboard operation
ferry copied to clipboard

Subscription returns error when internet connection breaks

Open dopecoder opened this issue 4 years ago • 6 comments
trafficstars

Hi, As the mentioned in the docs, I have used the gql_websocket_link for connecting to web socket through ferry.

When a live subscription is in progress and a list is being displayed for example, And if the internet connection breaks intermittently or for a prolonged period. The subscription should ideally return the cached data, but currently a link exception is occurring with the following error.

ServerException(originalException: WebSocketChannelException: WebSocketChannelException: SocketException: Failed host
lookup: 'example.com' (OS Error: No address associated with hostname, errno = 7), parsedResponse: null)

And sometimes, this error:

ServerException(originalException: null, parsedResponse: null)

I believe it was working properly previously once when I tested it a while ago, But doesn't seem to be working now. Not sure if it is an issue that exists or happening in only my codebase.

dopecoder avatar Sep 26 '21 06:09 dopecoder

Receiving the error seems like the expected behavior. However, we should probably maintain the latest data in the response when there is a LinkException.

smkhalsa avatar Sep 29 '21 21:09 smkhalsa

Hi @smkhalsa, thank you for your response. If it returned the latest data, it would have been fine, but the response.data is null. But when I close the existing subscription and create a new one, it works fine.

Also, another thing I observed was that, After creating a new subscription when offline, it throws the same error with response.data = null when the internet comes back on.

Any way to fix this?

dopecoder avatar Nov 04 '21 01:11 dopecoder

Hi @smkhalsa, Any update on this?

dopecoder avatar Jan 16 '22 16:01 dopecoder

It sounds like there are really two issues here:

  1. Ferry doesn't include the latest with link errors.

  2. The websocket link you are using doesn't allow data events after error events.

I'm open to PRs for #1. #2 is unrelated to ferry.

smkhalsa avatar Jan 16 '22 18:01 smkhalsa

Hi @smkhalsa, My Current Workaround for sending cache data along with link exception is by enriching the response with cache data in the event of link exception.

case FetchPolicy.CacheAndNetwork:
        final sharedNetworkStream =
            _optimisticLinkTypedLink.request(operationRequest).shareValue();

        return _cacheTypedLink
            .request(operationRequest)
            .where((response) => response.data != null)
            .takeUntil(
          sharedNetworkStream.doOnData(
            (_) {
              /// Temporarily add a listener so that [sharedNetworkStream] doesn't shut down when
              /// switchMap is updating the stream.
              final sub = sharedNetworkStream.listen(null);
              Future.delayed(Duration.zero).then((_) => sub.cancel());
            },
          ),
        ).concatWith([
          sharedNetworkStream
              .doOnData(_removeOptimisticPatch)
              .doOnData(_writeToCache)
              .asyncMap(_enrichExceptionWithCache)  // added the function to add the cache data
              .switchMap(
                (networkResponse) => ConcatStream([
                  Stream.value(networkResponse),
                  _cacheTypedLink.request(operationRequest).skip(1),
                ]),
              )
        ]);

Here I check if it was a link exception and add the response data

  bool _isLinkException<TData, TVars>(
      OperationResponse<TData, TVars> response) {
    if (response.linkException != null) {
      print('_isLinkException : ${response.linkException}');
    }
    return response.linkException != null;
  }

  Future<OperationResponse<TData, TVars>>
      _enrichExceptionWithCache<TData, TVars>(
          OperationResponse<TData, TVars> response) async {
    if (_isLinkException(response)) {
      return OperationResponse<TData, TVars>(
        operationRequest: response.operationRequest,
        extensions: response.extensions,
        graphqlErrors: response.graphqlErrors,
        linkException: response.linkException,
        data: _cacheTypedLink.cache.readQuery(response.operationRequest),
      );
    }
    return response;
  }

Although this doesn't entirely fix it. It should at least work as expected if the link works as expected. Currently the link returns multiple times in the stream with the exception along with data. The downside is that the app re-renders when the stream returns multiple times(can workaround this by adding Stream.distinct()).

Please let me know your thoughts about this.

dopecoder avatar Jan 19 '22 02:01 dopecoder

Just stumbled now upon this. Offline is completly fine on http, but having troubles with websocket returning cached data. this is my implementation

static Link webSocketLink({
    Map<String, String>? defaultHeaders = const {},
    ChannelGenerator? testChannelGenerator,
    Duration? testInactivityTimeout,
    Duration testReconnectTimeout = const Duration(seconds: 3),
  }) {
    final uri = Uri.parse(Endpoints.graphql);
    final wsEndpointUri = uri.replace(scheme: uri.scheme == 'https' ? 'wss' : 'ws');

    WebSocketChannel? channel;
    final channelGenerator = testChannelGenerator != null
        ? (() async => channel = await testChannelGenerator()) as ChannelGenerator
        : () {
            log.finest('Creating GraphQL web socket, uri=$wsEndpointUri');
            return channel = WebSocketChannel.connect( //breaks right here
              wsEndpointUri,
              protocols: ['graphql-ws'],
            );
          };

    // If authentication state changes, we reconnect the socket, which will also
    // re-evaluate the initialPayload to provide the auth header if available.
    ClientService.client.auth.addTokenChangedCallback(() {
      if (channel != null) {
        log.finest('Reconnecting GraphQL web socket as result of token change');
        channel?.sink.close(webSocketNormalCloseCode, 'Auth changed');
      }
    });

    final webSocketLink = WebSocketLink(
      /* url — provided via channelGenerator */ null,
      autoReconnect: true,
      channelGenerator: channelGenerator,
      initialPayload: () => {
        'headers': {
          ...?defaultHeaders,
          if (ClientService.client.auth.authenticationState == AuthenticationState.signedIn)
            'Authorization': 'Bearer ${ClientService.client.auth.accessToken}',
        },
      },
      inactivityTimeout: testInactivityTimeout,
      reconnectInterval: testReconnectTimeout,
    );

    return webSocketLink;
  }

i have link split if operation subscription that goes to websocket or http. I tried to create another split instead of websocket link to compare if online to forward to websocket, then its not working and it does not matter if online or offline.

MichalNemec avatar Mar 29 '23 11:03 MichalNemec