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

forward in onError link doesn't send any requests #9609

Open
frimuchkov opened this issue Apr 18, 2022 · 8 comments
Open

forward in onError link doesn't send any requests #9609

frimuchkov opened this issue Apr 18, 2022 · 8 comments

Comments

@frimuchkov
Copy link

Hi! We try to refetch network errors but looks like forward doesn't work and doesn't emit new request.

const promiseToObservable = (promise: Promise<any>) => {
  return new Observable((subscriber: any) => {
    promise.then(
      (value) => {
        if (subscriber.closed) return;
        subscriber.next(value);
        subscriber.complete();
      },
      (err) => subscriber.error(err)
    );
  });
};

const errorLink = onError(({ graphQLErrors, networkError, forward, operation }) => {
  if (graphQLErrors) {
    const userErrors = graphQLErrors
      .filter(({ message }) => !SYSTEM_ERRORS.some(systemError => message.indexOf(systemError) > -1))
    userErrors.slice(0, 3).forEach(({ message }) => GlobalSnackbars.error(message));
    graphQLErrors.map(e => console.error(`[GraphQL Error] ${e.message}`, e))

    if (graphQLErrors.some(({ message }) => message.indexOf('ECONNREFUSED') > -1)) {
      return promiseToObservable(new Promise(resolve => setTimeout(resolve, 1000))).flatMap(() => {
        return forward(operation)
      });
    }
  }
});
@magrinj
Copy link

magrinj commented Jul 5, 2022

@frimuchkov Did you find a way to fix this ?
I'm facing the same issue when trying to refresh my token in the onError link

@magrinj
Copy link

magrinj commented Jul 6, 2022

@frimuchkov Problem solved on my side, it was just a problem of order in ApolloLink.from.
I'm using onError to handle refresh token, and actually it does retry on forward, but if the same error happen it stop to avoid endless loop.
My problem was that my authLink was set after my errorLink, so when an error happen the authLink is called on forward operation and the old token is used instead of the new one.
Just putting the authLink before the errorLink fix everything.

@frimuchkov
Copy link
Author

@magrinj have you solved it with chaning order of links?

@magrinj
Copy link

magrinj commented Jul 6, 2022

@frimuchkov Yes in my case it was the issue, I've 3 links httpLink, errorLink and authLink.
httpLink does basic stuff
authLink is a setContext for the accessToken
errorLink handle Unauthenticated error, refresh the token and set a new context before forwarding the operation.
The initial order was:

ApolloLink.from([errorLink, authLink, httpLink]); // -> Not working

The correct order is:

ApolloLink.from([authLink, errorLink, httpLink]); // -> Working

In the wrong order, setContext of authLink is called again right after forward, so the wrong token is set again.
As the same error is thrown because of the wrong token, it doesn't go again in onError to avoid endless loop, it's why I first think that forward was not working.

@jpvajda
Copy link
Contributor

jpvajda commented Jul 15, 2022

@frimuchkov 👋 Let us know if the suggestions from @magrinj helped your case. Thanks for the guidance @magrinj, it's appreciated. 🙏

@jpvajda jpvajda added 🏓 awaiting-contributor-response requires input from a contributor ⁉️ question labels Jul 15, 2022
@moeldeeb98
Copy link

moeldeeb98 commented Aug 31, 2022

i still face the problem when token get expired
i send a request to get new valid token and refreshToken up till now it is okay
it should retry the request the get unauthorized so i call ==> return forward(operation); in onError function but it doesn't work
i don't get any retry request in network

const BASE_URL = process.env.NEXT_PUBLIC_API_URL;

const httpLink = new HttpLink({ uri: BASE_URL });

const authMiddleware = new ApolloLink((operation, forward) => {
  // add the authorization to the headers
  operation.setContext(({ headers = {} }) => ({
    headers: {
      ...headers,
      authorization: `bearer ${getLocalAccessToken()}`,
    },
  }));

  return forward(operation);
});

const handleError = onError(({ graphQLErrors, networkError, operation, forward }: any) => {
  if (graphQLErrors) {
    for (let err of graphQLErrors) {
      switch (err.extensions.code) {
        case 'UNAUTHENTICATED':
          console.log('==UNAUTHENTICATED==');
          refreshToken().then(({ data }: any) => {
            const { token, refresh } = data.refreshToken;
            updateLocalAccessToken(token);
            updateLocalRefreshToken(refresh.refreshToken);

            const oldHeaders = operation.getContext().headers;
            operation.setContext({
              headers: {
                ...oldHeaders,
                authorization: `bearer ${token}`,
              },
            });
            return forward(operation);
          });
      }
    }
  }
  if (networkError) {
    store.dispatch(showAlert('Connection Failed, please try again!', 'error'));
  }
});

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: ApolloLink.from([authMiddleware, handleError, httpLink]),
});

@jpvajda jpvajda removed the 🏓 awaiting-contributor-response requires input from a contributor label Sep 1, 2022
@ohsofoxy273
Copy link

ohsofoxy273 commented Jan 23, 2023

@moeldeeb98 - I had similar code to yours, using Firebase for my client side token and verifying it on the backend with Firebase admin. After returning an "Observable" from apollo/client, the request was forwarded. The code is courtesy of stackoverflow users Igor Lamos and Dan Dascalescu

import { ApolloClient } from 'apollo-client';
import { onError } from 'apollo-link-error';
import { ApolloLink, Observable } from 'apollo-link';  // add Observable

// Define Http link
const httpLink = new createHttpLink({
  uri: '/my-graphql-endpoint',
  credentials: 'include'
});

// Add on error handler for apollo link

return new ApolloClient({
  link: ApolloLink.from([
    onError(({ graphQLErrors, networkError, operation, forward }) => {
      // User access token has expired
      if (graphQLErrors && graphQLErrors[0].message === 'Unauthorized') {
        // We assume we have both tokens needed to run the async request
        if (refreshToken && clientToken) {
          // Let's refresh token through async request
          return new Observable(observer => {
            authAPI.requestRefreshToken(refreshToken, clientToken)
              .then(refreshResponse => {
                operation.setContext(({ headers = {} }) => ({
                  headers: {
                    // Re-add old headers
                    ...headers,
                    // Switch out old access token for new one
                    authorization: `Bearer ${refreshResponse.access_token}` || null,
                  }
                }));
              })
              .then(() => {
                const subscriber = {
                  next: observer.next.bind(observer),
                  error: observer.error.bind(observer),
                  complete: observer.complete.bind(observer)
                };

                // Retry last failed request
                forward(operation).subscribe(subscriber);
              })
              .catch(error => {
                // No refresh or client token available, we force user to login
                observer.error(error);
              });
          });
        }
      }
    })
  ])
});

@mirko-console-dir
Copy link

mirko-console-dir commented Oct 29, 2023

Hello everyone, I encountered a similar issue, forget forward instead use axios.

const errorLink = onError(({ graphQLErrors,networkError, operation }) => {
try{
if (graphQLErrors) {
graphQLErrors.map(({ message, extensions }) => {
console.log([GraphQL error]: Message: ${message});
console.log('extensions.code',extensions.code);

    if (extensions && extensions.code === 'UNAUTHENTICATED') {
      return refreshToken().then(async(newAccessToken) => {
        const oldHeaders = operation.getContext().headers;
        
        // Update context with the new token
        operation.setContext({
            headers: {
                ...oldHeaders,
                authorization: newAccessToken ? `Bearer ${newAccessToken}` : "",
            },
        });

        // Get the necessary parts of the operation
        const query = operation.query.loc.source.body;
        const variables = operation.variables;
        const operationName = operation.operationName;
        
        // Sending the operation manually using axios
        return axios({
            url: 'http://localhost:4000/graphql',
            method: 'POST',
            headers: {
                ...oldHeaders,
                authorization: newAccessToken ? `Bearer ${newAccessToken}` : "",
            },
            data: {
                query,
                variables,
                operationName
            }
        })
        .then(response => {
            // Handle the response as needed
            console.log('Response:', response.data);
             // Manually write the data to Apollo's cache
            client.writeQuery({
              query: operation.query,
              variables: operation.variables,
              data: response.data.data,
            });
        })
        .catch(error => {
            console.error('Error sending operation:', error);
        });
    });

});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants