apollo-client
apollo-client copied to clipboard
forward in onError link doesn't send any requests
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)
});
}
}
});
@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
@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.
@magrinj have you solved it with chaning order of links?
@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.
@frimuchkov 👋 Let us know if the suggestions from @magrinj helped your case. Thanks for the guidance @magrinj, it's appreciated. 🙏
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]),
});
@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);
});
});
}
}
})
])
});
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);
});
});
});