Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

React Native Apollo Client does not trigger subscriptions' events #130

Closed
rgbedin opened this issue Mar 6, 2021 · 13 comments
Closed

React Native Apollo Client does not trigger subscriptions' events #130

rgbedin opened this issue Mar 6, 2021 · 13 comments
Labels
unrelated Not relevant, related or caused by the lib

Comments

@rgbedin
Copy link

rgbedin commented Mar 6, 2021

Hello everyone! Thanks for making this library and providing support for the community.

I am trying to use it in a React Native application but it seems I am doing something wrong, because the subscriptions are fired by the server, but I don't receive anything on my client.

Please note that I migrated from the old subscriptions-transport-ws library - everything was working fine using that and the WebSocketLink implementation by @apollo/client/link/ws. However I decided to migrate since that library seems to not have been in active development and the docs recommended using this new one. Moreover, I am having issues with the connection dropping on mobile with the old lib. This is why I wanted to give this one a shot.

This is my WebSocketLink implementation:

import { ApolloLink, Operation, FetchResult, Observable } from '@apollo/client';
import { print } from 'graphql';
import { createClient, ClientOptions, Client } from 'graphql-ws';

export class WebSocketLink extends ApolloLink {
  private client: Client;

  constructor(options: ClientOptions) {
    super();
    this.client = createClient(options);
  }

  public request(operation: Operation): Observable<FetchResult> {
    return new Observable((sink) => {
      return this.client.subscribe<FetchResult>(
        { ...operation, query: print(operation.query) },
        {
          next: sink.next.bind(sink),
          complete: sink.complete.bind(sink),
          error: (err) => {
            if (err instanceof Error) {
              return sink.error(err);
            }
          }
        }
      );
    });
  }
}

And this is how I am creating the link:

  const buildLinks = (): ApolloLink => {
    const httpLink = createHttpLink({
      uri: () => {
        return envRef.current?.graphqlUri || ''; // Should never be undefined, so that's fine
      }
    });

    const wsLink = new WebSocketLink({
      url: envRef.current?.graphqlWsUri || '',
      connectionParams: () => {
        return {
          authorization: `Bearer ${storedSessionRef.current?.accessToken}`,
          orgguid: storedSessionRef.current?.orgGuid
        };
      }
    });

    const splitLink = split(
      ({ query }) => {
        const definition = getMainDefinition(query);
        return (
          definition.kind === 'OperationDefinition' &&
          definition.operation === 'subscription'
        );
      },
      wsLink,
      httpLink
    );

    const retryLink = new RetryLink({ attempts: { max: Infinity } });

    const errorLink = onError(({ graphQLErrors, networkError }) => {
      if (graphQLErrors) {
        for (const err of graphQLErrors) {
          switch (err.extensions?.code) {
            case 'UNAUTHENTICATED':
              setIsUnauthorized(true);
              break;
            default:
              console.warn(
                `GraphQL error: Message: ${
                  err.message
                }, Location: ${JSON.stringify(err.locations)}, Path: ${
                  err.path
                }`
              );
              addError(err);
              break;
          }
        }
      }
      if (networkError) {
        console.warn(`GraphQL network error: ${networkError}`);
        addError(undefined, networkError);
      }
    });

    const authLink = setContext((_, { headers }) => {
      return {
        headers: {
          ...headers,
          authorization: storedSessionRef.current?.accessToken
            ? `Bearer ${storedSessionRef.current?.accessToken}`
            : '',
          orgGuid: storedSessionRef.current?.orgGuid || ''
        }
      };
    });

    const link = from([
      authLink,
      errorLink,
      retryLink,
      splitLink
    ]);

    return link;
  };

In order to create the subscription, I am using the useSubscription hook by @apollo/client:

  useSubscription(MESSAGE_MODIFIED_SUBSCRIPTION, {
    onSubscriptionData({ subscriptionData }) {
      const modifiedMessage = subscriptionData.data
        .messageModified as ChannelMessageFieldsFragment;

      console.info(
        'TauriaSubscriptionContext: modified message received:',
        modifiedMessage.guid
      );

      setModifiedMessage(modifiedMessage);
    }
  });

Package versions:
@apollo/client: 3.3.11
react-native: 0.63.4

Could you give some insight on what I might be doing wrong? Again, this setup works fine if I use import { WebSocketLink } from "@apollo/client/link/ws". Thank you.

@enisdenjo
Copy link
Owner

Hey there, 2 questions:

  • Why are you ignoring non-Error instance errors in the WebSocketLink implementation? There cane be other error types, one which is very important is an instance of the CloseEvent that occurs on WebSocket connection errors.
  • What server side implementation are you using? The server must also use graphql-ws or implement the new GraphQL over WebSocket Protocol. If the server implements the unmaintained subscriptions-transport-ws Protocol, you must use the subscriptions-transport-ws client. The two Protocols are not interchangeable.

@enisdenjo enisdenjo added the question Further information about the library is requested label Mar 6, 2021
@rgbedin
Copy link
Author

rgbedin commented Mar 6, 2021

Hey there, 2 questions:

  • Why are you ignoring non-Error instance errors in the WebSocketLink implementation? There cane be other error types, one which is very important is an instance of the CloseEvent that occurs on WebSocket connection errors.
  • What server side implementation are you using? The server must also use graphql-ws or implement the new GraphQL over WebSocket Protocol. If the server implements the unmaintained subscriptions-transport-ws Protocol, you must use the subscriptions-transport-ws client. The two Protocols are not interchangeable.

Thank you for the promptly reply.

  1. I was actually implementing as defined in the documentation. I just removed to check if that would make it work for any reason. Sorry for missing that part in my original post.
  2. That is the issue, then. I thought they were interchangeable. I need to change my server-side, then.

Side question: do you have experience in using that library with mobile apps (i.e. React Native)? I am still checking if it is worthing migrating from subscriptions-transport-ws. Are there any documentation on what exactly are the differences between these two libraries?

Once more, thank you for helping.

@asmeikal
Copy link

asmeikal commented Mar 6, 2021

Hi @rgbedin! I'm in the process of migrating three React Native apps from subscriptions-transport-ws to graphql-ws, and the only issue I encountered was that some network errors have strange behavior (because of React Native, see this issue), and would stop the client with a fatal error when it shouldn't. @enisdenjo released in version 4.2.0 a way to handle this strange React Native behavior, and you can use the isFatalConnectionProblem client option with something as simple as () => false to have the client reconnect forever.

The backwards compatibility example in the README was what I followed to make the back-end compatible with graphql-ws without breaking the current version of the clients. I had some trouble with this since I'm using apollo-server-core and its installSubscriptionHandlers method (see here). I had to replicate the logic of that method outside of the class to get it to work, since it has some peculiar defaults on how to handle the server instance given in input.

@rgbedin
Copy link
Author

rgbedin commented Mar 6, 2021

Hi @rgbedin! I'm in the process of migrating three React Native apps from subscriptions-transport-ws to graphql-ws, and the only issue I encountered was that some network errors have strange behavior (because of React Native, see this issue), and would stop the client with a fatal error when it shouldn't. @enisdenjo released in version 4.2.0 a way to handle this strange React Native behavior, and you can use the isFatalConnectionProblem client option with something as simple as () => false to have the client reconnect forever.

The backwards compatibility example in the README was what I followed to make the back-end compatible with graphql-ws without breaking the current version of the clients. I had some trouble with this since I'm using apollo-server-core and its installSubscriptionHandlers method (see here). I had to replicate the logic of that method outside of the class to get it to work, since it has some peculiar defaults on how to handle the server instance given in input.

This is great, thank you for sharing. What was the motivation behind the migration? Did you experience connection issues using subscriptions-transport-ws on React Native? Right now it seems that randomly my application loses the subscription connection after some arbitrary number of time and does not reconnect back - that is not easily reproducible and sometimes it takes hours for it to happen. This is why I am leaning toward trying migrating as well.

Would you be able to prove your implementation of WebSocketLink for React Native? I understand if you are not able to do so; just thought I might try asking. 😄

@asmeikal
Copy link

asmeikal commented Mar 6, 2021

The WebSocketLink we are using is a copy paste of the example in this repo, so it looks like what you posted except for the custom error handling - we always call sink.error. It works and we didn't change it. :D

The reason for the switch was that after about a year of inexplicable bugs - mainly clients that did not receive any subscription even though they appeared to be connected - we discovered that the issue was this bug in subscriptions-transport-ws. The scenario for us was roughly this:

  • you create a client in lazy mode
  • you start a subscription, and the client does its initial handshake
  • the client loses the connection temporarily, and reconnects automatically
  • because of the bug linked above, the old subscription is re-established before the initial handshake for the new connection

In our system, the connection params sent with the initial handshake are stored inside the GraphQL context, and are accessed every time a subscription is established. Since subscriptions are re-established before the handshake completes, after a reconnection all our subscriptions would fail with something like cannot read property 'authorization' of undefined. The client remained connected, but it did not receive any new subscriptions.

@rgbedin
Copy link
Author

rgbedin commented Mar 6, 2021

I see, I believe this might be the issue happening on my side as well. I have not put a lot of investigation into that; I was under the assumption my client was not connected. From your description it's very likely the client might be connected, but does not receive the event -- because that was the only thing I was actually checking. Thank you for shining a light into this. We will probably proceed with the migration as well.

@enisdenjo
Copy link
Owner

Hey @rgbedin, sorry but the two Protocols are not interchangeable. The new one is actually a rewrite.

Sadly, I have no experience with React Native at all; therefore, cant really give you any pointers. However, I see that @asmeikal has shared some insights - thanks for that!

@enisdenjo
Copy link
Owner

Hey @rgbedin, if you're satisfied with the answers please close the issue; if not, feel free to ask more questions. 😄

@UglyHobbitFeet
Copy link

UglyHobbitFeet commented Apr 23, 2021

If the server implements the unmaintained subscriptions-transport-ws Protocol, you must use the subscriptions-transport-ws client. The two Protocols are not interchangeable.

Wish this was posted somewhere easier to find. Spent hours trying to figure out why I couldn't connect with the default client example shown in the readme. My backend is a java GraphQL server using com.netflix.graphql.dgs and I'm guessing this may be the issue as I can connect fine using apollo-client (through vue-apollo). Anytime I try to subscribe using graphql-ws it fails with a 1006 error code.

@enisdenjo
Copy link
Owner

enisdenjo commented Apr 23, 2021

The server implementation in graphql-ws does log a warning in the console when it guesses that the Protocol is not supported. Your backend, sadly, does not handle wrong subprotocols gracefully. The code 1006 indicates abrupt, unhandled, closure, it is reserved for abruptly terminating the connection without warning or close reason.

But yeah, I get that rather often. I, therefore, added a disclaimer right in the main readme.

@enisdenjo enisdenjo added unrelated Not relevant, related or caused by the lib and removed question Further information about the library is requested labels May 31, 2021
@BraveEvidence
Copy link

@rgbedin Were you able to solve the issue? @asmeikal Even after adding isFatalConnectionProblem: () => false, does not work for me. Can you check #220

@rgbedin
Copy link
Author

rgbedin commented Aug 10, 2021

I have put this on hold and did not make further investigations on it. Sorry.

@BraveEvidence
Copy link

@rgbedin ok thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
unrelated Not relevant, related or caused by the lib
Projects
None yet
Development

No branches or pull requests

5 participants