- This is a query statement that retrieves user information. If error 401 occurs here
import { ApolloError, gql, useQuery } from '@apollo/client';
interface IUserInfo {
fetchUser: {
picture: string;
nickname: string;
email: string;
};
}
const GET_USER = gql`
query {
fetchUser {
picture
nickname
email
}
}
`;
const useUser = () => {
const { data, error, loading, refetch } = useQuery<IUserInfo>(GET_USER, {
onError(error) {},
});
return {
user: data?.fetchUser,
error: error?.graphQLErrors[0]?.originalError,
loading,
};
};
export default useUser;
- This is where you send the request to the server.
import {
ApolloClient,
ApolloError,
ApolloLink,
ApolloProvider,
InMemoryCache,
Observable,
createHttpLink,
fromPromise,
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';
import { RESTORE_TOKEN_MUTATION } from './graphql/mutations/restore-token-mutation';
const httpLink = createHttpLink({
uri: 'http://localhost:3001/graphql',
});
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('token');
return {
headers: {
...headers,
Authorization: token ? `Bearer ${token}` : '',
},
};
});
const restoreAccessToken = async () => {
try {
const response = await client.mutate({
mutation: RESTORE_TOKEN_MUTATION,
});
console.log('Token refreshed:', response);
return response.data;
} catch (error: any) {
console.error('Token refresh error:', error);
throw new ApolloError({
graphQLErrors: [error],
networkError: error,
});
}
};
const errorLink = onError(({ graphQLErrors, operation, forward }) => {
if (graphQLErrors) {
for (let err of graphQLErrors) {
if (err.message === 'Unauthorized') {
return fromPromise(
restoreAccessToken()
.then((token) => {
localStorage.setItem('token', token);
const oldHeaders = operation.getContext().headers;
operation.setContext({
headers: {
...oldHeaders,
Authorization: `Bearer ${token}`,
},
});
return forward(operation);
})
.catch((error: any) => {
console.error('Refresh token error:', error);
return;
})
).concat(forward(operation));
}
}
}
});
const client = new ApolloClient({
link: ApolloLink.from([errorLink, authLink, httpLink]),
cache: new InMemoryCache(),
});
const ApolloSetting = ({ children }: { children: React.ReactNode }) => {
return <ApolloProvider client={client}>{children}</ApolloProvider>;
};
export default ApolloSetting;
But the server keeps sending 401 errors endlessly!
Here’s what I expected:
-
When the accesstoken expires, a 401 error is thrown.
-
If a 401 error is received, the front desk sends a re-request to the server (via the refresh token in the header)
-
The server receives the refresh token stored in headers.cookie and issues and sends a new accessToken if the validity period remains.
-
If req.headers.cookie does not exist or refresh token authentication fails, an error is sent again.
This is what I expected.
How can I escape infinite error?
My server is nestjs + graphql.
// - resolver..
@UseGuards(GqlAuthGuard('refresh'))
@Mutation(() => String)
restoreAccessToken(
@Context() context: IContext, //
): string {
const user = context.req.user;
return this.authService.restoreAccessToken({ user });
}
// - jwt
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-jwt';
export class JwtRefreshStrategies extends PassportStrategy(
Strategy,
'refresh',
) {
constructor() {
super({
jwtFromRequest: (req) => {
let refreshToken = null;
if (req && req.headers.cookie) {
const cookie = req.headers.cookie;
console.log('cookie:', cookie);
const cookieParts = cookie.split(';').map((c) => c.trim());
const refreshTokenCookie = cookieParts.find((c) =>
c.startsWith('refreshToken='),
);
if (refreshTokenCookie) {
refreshToken = refreshTokenCookie.substring('refreshToken='.length);
}
}
return refreshToken;
},
secretOrKey: process.env.JWT_REFRESH_KEY, // secretKey
});
}
validate(payload) {
console.log('payload::', payload);
return {
id: payload.sub,
};
}
}