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

Apollo v3 subscriptions not working on React Native Android only. #6520

Closed
immortalx opened this issue Jul 1, 2020 · 51 comments
Closed

Apollo v3 subscriptions not working on React Native Android only. #6520

immortalx opened this issue Jul 1, 2020 · 51 comments

Comments

@immortalx
Copy link

Just trying to upgrade to v3.

Everything is working fine, just subscriptions on React Native android doesn't work at all. On iOS is working fine as expected.
On v2, both iOS and Android works fine.

Intended outcome:
Subscriptions should work on both iOS and Android while using React Native.

Actual outcome:
Subscriptions only works in iOS. On android nothing happens, no errors or warnings.

Versions
npmPackages:
@apollo/client: ^3.0.0-rc.9 => 3.0.0-rc.9
@apollo/link-context: ^2.0.0-beta.3 => 2.0.0-beta.3
@apollo/link-error: ^2.0.0-beta.3 => 2.0.0-beta.3
@apollo/link-retry: ^2.0.0-beta.3 => 2.0.0-beta.3
@apollo/link-ws: ^2.0.0-beta.3 => 2.0.0-beta.3
apollo-cache-persist: ^0.1.1 => 0.1.1

@mschipperheyn
Copy link

mschipperheyn commented Jul 1, 2020

I am testing on Android right now. It does work for me. What's your setup?

@martimalek
Copy link

I'm also experiencing this issue. I'm using the basic setup from the docs:

const httpLink = createHttpLink({ uri: `${API_URL}/graphql` });

const wsLink = new WebSocketLink({
    uri: `${SUBSCRIPTION_API_URL}/graphql`,
    options: {
        reconnect: true,
    },
});

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

the split is working as expected but when using useSubscription everything in the response is undefined except the loading which is always true. On GraphiQL and on React (non-native) it is working perfectly.

@benjamn
Copy link
Member

benjamn commented Jul 1, 2020

Can you try different beta/RC versions (beta.0-beta.54, rc.0-rc.10) to determine if this was a regression introduced in a specific release? A binary search will probably save time, but I would check beta.0 first to see if it was working, before you do a full search.

@benjamn benjamn added this to the Release 3.0 milestone Jul 1, 2020
@immortalx
Copy link
Author

@benjamn You're right,it's a regression! Just tried a few beta and RC versions, on 3.0.0-beta.23 it works as expected.

My setup:

let persistor
export const wsClient = new SubscriptionClient(WS_URI, {
  lazy: true,
  reconnect: true,
  connectionParams: async () => {
    const token = await AsyncStorage.getItem(authToken)
    return {
      Authorization: token ? `Bearer ${token}` : null
    }
  }
})
const wsLink = new WebSocketLink(wsClient)
export const setupApollo = async () => {
  const retryLink = new RetryLink({
    attempts: (count, operation, error) => {
      const isMutation =
        operation &&
        operation.query &&
        operation.query.definitions &&
        Array.isArray(operation.query.definitions) &&
        operation.query.definitions.some(
          (def) =>
            def.kind === 'OperationDefinition' && def.operation === 'mutation'
        )
      if (isMutation) {
        return !!error && count < 25
      }
      return !!error && count < 6
    }
  })
  const httpLink = new HttpLink({
    uri: GRAPHQL_URI,
    credentials: 'include'
  })

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

  const errorLink = onError(({ graphQLErrors, networkError }) => {
    if (graphQLErrors)
      graphQLErrors.map(({ message, locations, path }) => {
        console.log(
          `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
        )
      })
    if (networkError) console.log(`[Network error]: ${networkError}`)
  })

  const authLink = setContext(async (_, { headers }) => {
    const token = await AsyncStorage.getItem(authToken)
    return {
      headers: {
        ...headers,
        authorization: token ? `Bearer ${token}` : null
      }
    }
  })
  const cache = new InMemoryCache({
    possibleTypes: introspectionResult.possibleTypes
  })
  persistor = new CachePersistor({
    cache,
    storage: AsyncStorage as PersistentStorage<
      PersistedData<NormalizedCacheObject>
    >,
    maxSize: 2048000,
    // debug: true,
    trigger: 'background'
  })

  const currentVersion = await AsyncStorage.getItem(SCHEMA_VERSION_KEY)
  try {
    if (currentVersion === SCHEMA_VERSION) {
      // If the current version matches the latest version,
      // we're good to go and can restore the cache.
      await persistor.restore()
    } else {
      // Otherwise, we'll want to purge the outdated persisted cache
      // and mark ourselves as having updated to the latest version.
      await persistor.purge()
      await AsyncStorage.setItem(SCHEMA_VERSION_KEY, SCHEMA_VERSION)
    }
  } catch (err) {
    // CachePersistor failed - purge
    await persistor.purge()
  }

  return new ApolloClient({
    link: from([errorLink, authLink, retryLink, splitLink]),
    cache,
    assumeImmutableResults: true,
    queryDeduplication: true,
    defaultOptions: {
      watchQuery: {
        fetchPolicy: 'cache-and-network',
        errorPolicy: 'none'
      },
      query: {
        fetchPolicy: 'cache-first',
        errorPolicy: 'all'
      }
    }
  })
}

@immortalx
Copy link
Author

@benjamn Lastest working beta is beta.45, i hope it helps.

@martimalek
Copy link

@immortalx is correct, beta.46 is the first one where it breaks

@benjamn
Copy link
Member

benjamn commented Jul 13, 2020

@immortalx Any chance you could put your code into a runnable (React Native) reproduction repo/sandbox? I wish I could link you to a simple RN/Apollo starter project, but I don't know of one off-hand. Presumably https://expo.io/ is the easiest way to get started, and maybe you can find a "snack" that uses @apollo/client? Curious to see what you find.

Note that AC3 is slated to be released tomorrow, so this might not get fixed before then, but that doesn't mean we won't fix it.

@immortalx
Copy link
Author

@benjamn sure, can you point me to any public graphql api with subscriptions so i can use it to test?

@mikeislearning
Copy link

mikeislearning commented Aug 2, 2020

Thanks for bringing this up @immortalx, I was starting to go nuts. I can confirm that this is still broken in the latest 3.1.1 version, and I'm now using @apollo/client^3.0.0-beta.45 to get subscriptions working on android

@schonfeld
Copy link

Same in v3.1.3. Same Subscriptions code that worked fine in v2 on both {iOS, Android} is now broken on Android. The loading flag is just stuck at true.

@schonfeld
Copy link

Worth noting that when rn's Debug mode is enabled, causing JS to be parsed in the browser context (as opposed to the "device" or bridge), subscriptions work just fine.

@schonfeld
Copy link

Some useful insight: turning off Hermes engine actually fixed the issue. This definitely smells like a js engine change/issue, rather than Apollo client problem.

@kirantripathi
Copy link

Screen Shot 2020-08-29 at 19 59 05

Screen Shot 2020-08-29 at 19 58 52

Screen Shot 2020-08-29 at 19 58 27

My subscription hook is just returning loading as response. I am using "subscriptions-transport-ws": "^0.9.18". Do you guys have any idea about how to fix it? @schonfeld @benjamn @immortalx @martimalek

@schonfeld
Copy link

Screen Shot 2020-08-29 at 19 59 05 Screen Shot 2020-08-29 at 19 58 52 Screen Shot 2020-08-29 at 19 58 27

My subscription hook is just returning loading as response. I am using "subscriptions-transport-ws": "^0.9.18". Do you guys have any idea about how to fix it? @schonfeld @benjamn @immortalx @martimalek

If this only happens on Android for you, my solution was turning off Herms engine in android/app/build.gradle:

project.ext.react = [
    entryFile: "index.js",
    enableHermes: false
]

If the issue also occurs on an iPhone sim (or device), your problem might not relate to this specific issue...

@kirantripathi
Copy link

kirantripathi commented Aug 29, 2020

Screen Shot 2020-08-29 at 19 59 05 Screen Shot 2020-08-29 at 19 58 52 Screen Shot 2020-08-29 at 19 58 27 My subscription hook is just returning loading as response. I am using "subscriptions-transport-ws": "^0.9.18". Do you guys have any idea about how to fix it? @schonfeld @benjamn @immortalx @martimalek

If this only happens on Android for you, my solution was turning off Herms engine in android/app/build.gradle:

project.ext.react = [
    entryFile: "index.js",
    enableHermes: false
]

If the issue also occurs on an iPhone sim (or device), your problem might not relate to this specific issue...

No, it is happening in both ios and android and I haven't enable hermes too

@happyfloat
Copy link

Having the same problem with hermes. It's only working in debug mode because of the different js engine... damn...

@stackia
Copy link

stackia commented Sep 2, 2020

Any updates? Subscription is still broken on Hermes engine.

@Rieranthony
Copy link

Any news at all? This issue is blocking our Android app, would love to start using v3 but this is a huge blocker :( Thanks!

@ryoid
Copy link

ryoid commented Sep 23, 2020

Also experiencing this issue, works on iOS. Using hermes engine for android.

"react-native": "0.63.2",
"@apollo/client": "^3.2.0",
"subscriptions-transport-ws": "^0.9.18"

@rarenatoe
Copy link

rarenatoe commented Sep 28, 2020

I already have hermes disabled by default. NEVER USED IT.

I managed to get it back working. In my case, the error seems to have been that one of my susbscriptions didn't have an OperationName, i.e. it was written as:

export const mySubscription = gql`
  subscription {
    subscriptionName {
      id
      ...etc
    }
  }
`

This was fixed by giving it an operationName such as:

export const mySubscription = gql`
  subscription mySubscription {
    subscriptionName {
      id
      ...etc
    }
  }
`

In my case it just so happened that this subscription was the first one ever called and so for whatever reason it broke everything subscription-related after it, even when other subscriptions did have an OperationName.

For whatever reason Apollo 3 now requires all subscriptions to have an OperationName, which definitely was never the case in 2.X. This was never mentioned anywhere, and let's just say I am upset about the time I had to waste.

@therealemjy
Copy link

The error persists for me on Apollo v3, even when giving the subscription an OperationName:

const MESSAGE_ADDED_SUBSCRIPTION = gql`
  subscription messageAdded($chatIds: [ID]!) {
    messageAdded(chatIds: $chatIds) {
      _id
      createdAt
      senderId
      chatId
      content
    }
  }
`;

The issue happens only on Android when Hermes engine is enabled.

@dvirben123
Copy link

The error persists for me on Apollo v3, even when giving the subscription an OperationName:

const MESSAGE_ADDED_SUBSCRIPTION = gql`
  subscription messageAdded($chatIds: [ID]!) {
    messageAdded(chatIds: $chatIds) {
      _id
      createdAt
      senderId
      chatId
      content
    }
  }
`;

The issue happens only on Android when Hermes engine is enabled.

same thing here, Apollo v3, can't get subscription working (thinking moving to socket.io or pusher)

@zakthedev
Copy link

Hi @dvirben123, This one is not related to the issue but can I use Subscriptions for push notifications on mobile? is the app going to still receive data after subscribing when the app is in the background or killed? and what strategy is used to push pending notifications after device is turned on?

You may or may not have the answers to all of these but I really want to understand the concept here so any clarifications would be appreciated. Thank you :)

@dvirben123
Copy link

Hi @dvirben123, This one is not related to the issue but can I use Subscriptions for push notifications on mobile? is the app going to still receive data after subscribing when the app is in the background or killed? and what strategy is used to push pending notifications after device is turned on?

You may or may not have the answers to all of these but I really want to understand the concept here so any clarifications would be appreciated. Thank you :)

Hi, basically I want to get notifications only when the app is live or in the background, I know that using Apple or Google services for push notification is a better approach, I want to know what is the maximum that I get can from Apollo

@kirantripathi
Copy link

kirantripathi commented Nov 12, 2020

subscription is inconsistent sometimes data is present and sometimes it is not returning data , and when in debug mode sometimes subscription doesn't return data.in apk file also it is not showing any new data. I have operation name too in my subscription. @zakthedev @dvirben123 @rarenatoe @Rieranthony @immortalx @benjamn .any idea on how to solve this, or how did you guys fix this?

@nikonhub
Copy link

I always use the latest versions if it works.

package.json

    "@apollo/client": "^3.2.2",
    "@apollo/link-context": "^2.0.0-beta.3",
    "@apollo/link-error": "^2.0.0-beta.3",
    "@apollo/link-ws": "^2.0.0-beta.3",
    "apollo-cache-persist-dev": "^0.2.1",
    "apollo-upload-client": "^14.0.0"
    "graphql": "^15.0.0",
    "graphql-tag": "^2.10.3"
    "react": "16.13.1",
    "react-apollo": "^3.0.0",
    "react-native": "0.63.3"
    "subscriptions-transport-ws": "^0.9.15"

ApolloClient (basic)

import { Platform } from 'react-native';
import { ApolloLink, split, Observable } from '@apollo/client/core';
import { WebSocketLink } from '@apollo/link-ws';
import { getMainDefinition } from '@apollo/client/utilities';
import { onError } from '@apollo/link-error';
import { setContext } from '@apollo/link-context';
import { createUploadLink } from 'apollo-upload-client';
...
      _wsLink = new WebSocketLink({
        uri: AppConfig.server.socketUrl,
        options: {
          lazy: true,
          reconnect: true,
          connectionParams: () => ({
            authorization: _currentAuthorization,
            name: MYAPPNAME,
            version: AppConfig.version,
          }),
        },
      });

      mainLink = split(
        // split based on operation type
        ({ query }) => {
          const { kind, operation } = getMainDefinition(query);
          return kind === 'OperationDefinition' && operation === 'subscription';
        },
        _wsLink,
        httpLink,
      );

build.gradle

        buildToolsVersion = "29.0.3"
        minSdkVersion = 23
        compileSdkVersion = 29
        targetSdkVersion = 29
        supportLibVersion = "28.0.0"
        androidMapsUtilsVersion = "0.5+"

Not sure what else would be relevant to you ?

@Dunos
Copy link

Dunos commented Nov 17, 2020

@nikosmonaut if you are using Apollo 3, you should probably drop all those link packages since they are now included in the main Apollo Client (check https://www.apollographql.com/docs/react/migrating/apollo-client-3-migration/ )

@kirantripathi
Copy link

@Dunos is subscription working perfectly in your application, if it is can you share your apollo setup and package imported so that I can replicate it and test it

@happyfloat
Copy link

@kirantripathi I created a gist for you with my working setup. It seems to be working very well and stable so far. (As long as I do not use hermes engine).

https://gist.github.com/happyfloat/c0ddfc933cfa6a40f71947ae0ff49767

@kirantripathi
Copy link

@Dunos @nikosmonaut , one thing I am getting in subscription query response is regarding token expire , so if our token is expire , how can I reconnect the web socket with new token. any idea on that?
I have added token expire logic for HTTP query but not sure on web socket.

@Dunos
Copy link

Dunos commented Nov 28, 2020

@kirantripathi the approach we use for authentication on websockets is to use a query parameter with the token in the WS uri, and refreshing the token and the uri using the "onError" events of the SubscriptionClient.

@kirantripathi
Copy link

@Dunos can you provide me example ?

@nikonhub
Copy link

nikonhub commented Nov 28, 2020

@kirantripathi It's not really important to have a stale token in subscriptions. Because the connection is done only once the first time. Nevertheless if you want to reconnect you'll have to send a new token.

First of all check this library subscriptions-transport-ws because it's behind all of it.

If you have followed the template from graphql docs, you would already have a base. Then use a function as connectionParams

      _wsLink = new WebSocketLink({
        uri: URI,
        options: {
          lazy: true,
          reconnect: true,
          connectionParams: () => ({
            authorization: YOURTOKEN,
          }),
        },
      });

Then when you get a expired token message replace your variable YOURTOKEN by the new one and reconnect your socket.

// This will close the current one and reconnect, calling your param function with new token
_wsLink.subscriptionClient.close(false);

Edit: Beware it's a broken library. issue. So don't rely only on the onConnect callback serverside. But check your context in resolvers aswell

@hannojg
Copy link

hannojg commented Nov 29, 2020

Tried many many different versions, just got it working on 3.0.0 beta 45, any version after that doesn't seem to work on android using hermes.

@kirantripathi
Copy link

@nikosmonaut I try without updating token , and it is working , so we won't need to update token after session expire. One thing is I am not able to find is the reason for inconsistency , however when I close the app and open again maybe it will reconnect again so that's why it start to work again, but sometimes even though I get information as reconnecting and reconnected the subscription don't work , and it only work after reloading the app. any idea on how can I tackle it .

@nikonhub
Copy link

@kirantripathi I can't say for sure. But when you go background with your app the connection gets disconnected no matter what after some time (~1min). So when you go active again the connection is initiated once more. Imagine you have an expired token it will not work.

@benjamn
Copy link
Member

benjamn commented Dec 2, 2020

re: #6520 (comment)

@nikosmonaut You're looking at code in the zen-observable library there.

Observable subclasses typically define a static Symbol.species property, so inherited Observable methods (like Observable.prototype.map) that need to create new Observable objects can use new this.constructor[Symbol.species](observer => ...) instead of just new this.constructor(observer => ...), which is important specifically when the subclass constructor takes different/incompatible arguments than the Observable constructor takes. As a hint to methods like map, the subclass should set Subclass[Symbol.species] = Observable to prevent the subclass's own (incompatible) constructor from being invoked with the observer => ... argument.

Apollo Client has one Observable subclass that needs Symbol.species here, and that code happens to have been introduced in @apollo/client@3.0.0-beta.46, so I'm pretty sure you're onto something here!

However, I'm having trouble guessing anything more about the problem, because Hermes purposefully does not implement Symbol.species, so I would expect the Object.defineProperty call to be skipped with Hermes enabled (because Symbol.species is falsy), unless Symbol.species has been imperfectly polyfilled somehow, perhaps somewhere in the React Native runtime code? Not defining the Concast[Symbol.species] property should be fine, as long as it stays undefined, because getSpecies(concast) just returns Observable in that case, which is what methods like map want.

I realize this was a complicated explanation that probably sounds borderline unhinged if you aren't familiar with the ECMAScript specification, but hopefully it inspires someone here to dig a little deeper into whatever React Native is doing with Symbol.species that doesn't play nicely with Hermes.

benjamn added a commit that referenced this issue Dec 2, 2020
An attempt to address the problems I speculated about in
#6520 (comment)

Theory: in the Hermes JS engine, Symbol.species is not defined, so
Object.defineProperty was not called, but perhaps "@@species" was getting
set somewhere else by a polyfill library, causing the zen-observable
library to fall back to "@@species" instead of using/ignoring the
nonexistent Symbol.species symbol.
@nikonhub
Copy link

nikonhub commented Dec 2, 2020

I had got to read it 3 times, but finally I got it.

Your suppositions are right. I think it's core-js that add @@species prop to Symbol => core-js observable which is indeed used by react-native => @react-native-community/cli => metro => metro-babel-register -> core-js

benjamn added a commit that referenced this issue Dec 3, 2020
An attempt to address the problems I speculated about in
#6520 (comment)

Theory: in the Hermes JS engine, Symbol.species is not defined, so
Object.defineProperty was not called, but perhaps "@@species" was getting
set somewhere else by a polyfill library, causing the zen-observable
library to fall back to "@@species" instead of using/ignoring the
nonexistent Symbol.species symbol.
benjamn added a commit that referenced this issue Dec 3, 2020
An attempt to address the problems I speculated about in
#6520 (comment)

Theory: in the Hermes JS engine, Symbol.species is not defined, so
Object.defineProperty was not called, but perhaps "@@species" was getting
set somewhere else by a polyfill library, causing the zen-observable
library to fall back to "@@species" instead of using/ignoring the
nonexistent Symbol.species symbol.
@benjamn
Copy link
Member

benjamn commented Dec 3, 2020

Alright everybody, if you run npm i @apollo/client@beta (or npm i @apollo/client@3.4.0-beta.1 specifically), you'll get a version that includes #7403, which may address some of the problems reported in this issue, such as #6520 (comment). I don't expect this to solve all problems at once, but it should help focus the discussion somewhat. Thanks!

@benjamn
Copy link
Member

benjamn commented Dec 3, 2020

Of course, if you're updating to v3.4 from v3.0.0-beta.45, you may want to read through the CHANGELOG.md to make sure you're on top of all the other changes since then. If that's totally infeasible, we could publish a one-off version based on -beta.46 to minimize the immediate pain of updating, but I'm hoping everyone will eventually update to the latest version once this issue is fixed.

@vamshi9666
Copy link

@benjamn yes. this beta fixed my issue in android (hermes enabled) with version 3.4.0-beta.3

@martimalek
Copy link

From 3.4.0-beta.1 it works for us, thank you @benjamn!

@hannojg
Copy link

hannojg commented Dec 17, 2020 via email

benjamn added a commit that referenced this issue Feb 5, 2021
An attempt to address the problems I speculated about in
#6520 (comment)

Theory: in the Hermes JS engine, Symbol.species is not defined, so
Object.defineProperty was not called, but perhaps "@@species" was getting
set somewhere else by a polyfill library, causing the zen-observable
library to fall back to "@@species" instead of using/ignoring the
nonexistent Symbol.species symbol.
@benjamn
Copy link
Member

benjamn commented Feb 5, 2021

The Symbol.species fix has been backported to @apollo/client@3.3.8, in case anyone was waiting for it (and unwilling/unable to use the v3.4 betas).

@quanndh
Copy link

quanndh commented Mar 18, 2021

For some reason, subcription not working for me =( loading state true, data and error are undefined. Im using apollo client 3.3.8. Im got data on playground however

@Necromant1k
Copy link

Necromant1k commented Jul 19, 2021

For some reason, subcription not working for me =( loading state true, data and error are undefined. Im using apollo client 3.3.8. Im got data on playground however

Having the same issue

"react": "17.0.1",
"react-native": "0.64.2"

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 1, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests