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

useSubscription has no way to re-connect after error #11304

Open
objectiveSee opened this issue Oct 18, 2023 · 8 comments · Fixed by #11927
Open

useSubscription has no way to re-connect after error #11304

objectiveSee opened this issue Oct 18, 2023 · 8 comments · Fixed by #11927

Comments

@objectiveSee
Copy link

I am testing my app when the internet is bad. My subscription fails and I get an error, but there is no function exported by the subscription hook that let's me retry the connection. Similar to the refetch property in useQuery. Currently, I am unable to have my application reconnect the subscription once it has failed. This seems to be a pretty major oversight in how subscriptions work, so hoping that there's a workaround.

Is there some way to trigger a re-subscription attempt?

  // Setup subscription
  // TODO: If the subscription fails (eg. no internet) then there isn't a way for it to succeed again :/
  // This needs to be fixed otherwise user will be stuck on profile page thinking that the Retry button doesn't work
  // because the subscription is never retried.
  const { data: subscriptionData, error: subscriptionError } =
  useSubscription<OnUserUpdatedSubscription, OnUserUpdatedSubscriptionVariables>(
    onUserUpdatedSubscription,
    {
      variables: { id: userId },
      onError: (error: ApolloError) => {
        console.log(`[APOLLO ERROR] User Subscription Failed: ${JSON.stringify(error)}`)
      }
    }
  );

  // Query for user data on mount
  const { data: queryData, loading: queryLoading, error: queryError, refetch } =
  useQuery<GetUserQuery, GetUserQueryVariables>(getUserQuery, {
    variables: { userId },
    fetchPolicy: 'network-only',
    notifyOnNetworkStatusChange: true,
    errorPolicy: 'none',
  });

I am using:

    "@apollo/client": "^3.6.9",
    "aws-appsync-subscription-link": "^3.1.2",
  const httpLinkWithAuth = ApolloLink.from([
    errorLink,
    authMiddleware,
    createAuthLink({ url, region, auth }),
    createSubscriptionHandshakeLink({ url, region, auth }, httpLink)
  ]);

  client = new ApolloClient({
    link: httpLinkWithAuth,
@bignimbus
Copy link
Contributor

Hi @objectiveSee 👋🏻 there are several different protocols that can handle GraphQL subscriptions, some of which are handled by modules we officially support. AWS AppSync packages are provided by a third party, however. The core Apollo Client API itself is not opinionated about how to handle Websockets connections, so I recommend looking at the aws-appsync-subscription-link docs for guidance on how to handle disconnect events in your applications.

@objectiveSee
Copy link
Author

objectiveSee commented Oct 30, 2023

@bignimbus do you have any perspective on this from Apollo's standpoint? I am curious how Apollo client handles re-connecting the underlying socket(s) that support the subscription. Does any link you have in mind handle this or is there additional code needed to support reconnecting? I am looking for alternatives in case AppSync doesn't work. It appear that the topic of re-connecting subscriptions is not discussed a lot.

@bignimbus
Copy link
Contributor

We'd like to make the experience around reconnecting more clear for users while accounting for the fact that different protocols/libraries will expose different and sometimes not completely analogous interfaces. Expect to see some improvements to the developer experience along these lines in future releases, but we have no concrete plans yet.

@github-actions github-actions bot removed the 🏓 awaiting-contributor-response requires input from a contributor label Oct 31, 2023
@jamiter
Copy link
Contributor

jamiter commented Nov 10, 2023

The action could be simply be a "hard refresh" and restart the subscription, independent of the underlying protocol or library. Just like useSubscription is implementation independent. Something like this:

const { reconnect } = useSubscription(
    onUserUpdatedSubscription,
    {
      variables: { id: userId },
      onError: (error: ApolloError) => {
        if (error === 'something specific') {
          reconnect()
        }
      }
    }
  );

Maybe it should be called restart or reconnect or resubscribe. The result should be a new subscription, as if useSubscription was called for the first time.

@jamiter
Copy link
Contributor

jamiter commented Nov 11, 2023

I think what I would like to achieve is what the shouldResubscribe option does, but manually.

To quote the docs:

Determines if your subscription should be unsubscribed and subscribed again when an input to the hook (such as subscription or variables) changes.

So onError or onComplete I could resubscribe again. The skip might do for now, but a more standardised way would be preferred of course.

More background info on my use case

What I'm implementing is a Subscription not based on WebSockets, but HTTP multipart requests, like documented here. This is handled nicely by Apollo Client, but I have to implemented this manually in my NextJS backend. Maybe I'll open source it someday.

The thing is, when hosting on Vercel, the HTTP request will timeout after a maximum of 5 minutes on the Pro plan. If this happens, I'll need to resubscribe.

I still have to determine if this errors or completes the subscription, but I would then like to simply resubscribe using a new HTTP request. Once I've figured this all out I'll make sure to update here.

@objectiveSee
Copy link
Author

Thank you for the response @jamiter. I agree that adding a reconnect property to the hook would be really helpful. What would it take to get this added into the Apollo library? In the meantime, I wrote a custom hook that handles the reconnect logic through the skip hack that I mentioned. I added the ability to automatically reconnect as well as exposing a reconnect function. Here is the code for anyone who is interested.

Separately, I am dealing with an issue where the websocket implementation that I am using does not support the onComplete callback which is causing its own issues 🤯


import { ApolloError, DocumentNode, OperationVariables, SubscriptionHookOptions, TypedDocumentNode, useSubscription } from '@apollo/client';
import { useCallback, useState } from 'react';

import { delay } from '../utilities/delay';

// ms between reconnect attempts
const RECONNECT_TIME = 10000;
const DELAY_2 = 1000;   // ms skip is set to true before reconnecting
const DELAY_1 = RECONNECT_TIME - DELAY_2; // ms before skip is set to false pre-skip

export function useReconnectingSubscription<TData = any, TVariables extends OperationVariables = OperationVariables>(
  subscription: DocumentNode | TypedDocumentNode<TData, TVariables>,
  options?: SubscriptionHookOptions<TData, TVariables>
) {
  const [internalSkip, setInternalSkip] = useState(false);

  const reconnect = useCallback(async () => {
    // Toggle the value of `skip` to force the subscription to reconnect.
    // The value must be true then false for the reconnect to work.
    await delay(DELAY_1);  // allow 1sec for the error to show up. Once skip:true is set, the error will be cleared.
    setInternalSkip(true);
    console.log('🔌⭐️ Delaying reconnect');
    await delay(DELAY_2);
    console.log('🔌⭐️ Reconnecting');
    setInternalSkip(false);
  }, []);

  // Internally handle errors by reconnecting and calling the user-provided onError callback
  const onError = useCallback(async (error: ApolloError) => {
    console.log(`[useReconnectingSubscription] Subscription Failed: ${JSON.stringify(error)}`);
    if ( options.onError ) options.onError(error);
    return reconnect();
  }, [options, reconnect]);


  // NOTE: AWS AppSync subscription implementation doesn't call onComplete
  // See: https://github.com/awslabs/aws-mobile-appsync-sdk-js/issues/759
  const onComplete = useCallback(() => {
    // TODO: Implement exponential backoff of retries once we can determine when a
    // successful subscription has been established. (eg. to reset the exponential backoff)
    console.log('😵😵😵😵😵😵😵😵😵 This never happends');
    if ( options.onComplete ) {
      options.onComplete();
    }
  }, [options]);

  // Merge customized options with the user-provided options
  const mergedOptions = {
    ...options,
    skip: options?.skip || internalSkip,
    onError,
    onComplete
  };

  const { data, loading, error } = useSubscription<TData, TVariables>(subscription, mergedOptions);

  return {
    data,
    loading,
    error,
    reconnect
  };
}

@silverprize
Copy link

Sadly I just recreate instance of apollo client for reconnect.

@phryneas
Copy link
Member

phryneas commented Jul 5, 2024

This will be added in 3.11 with #11927

@phryneas phryneas linked a pull request Jul 5, 2024 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants