Nuxt与Apollo集成认证时遭遇Store重置错误及自定义客户端配置问题
Hey there, let's break down how to fix that frustrating invariant violation error and polish your custom Apollo client setup for your Nuxt project.
1. Fixing the "Store reset while query was in flight" Error
This error happens because when you trigger onLogout() (which resets the Apollo Store) for an expired token, there are still pending GraphQL requests in the link chain. The store reset conflicts with these in-flight requests, causing the invariant violation. Here's how to resolve it:
a. Cancel In-Flight Requests Before Logout
Instead of forwarding the operation when a 401 is detected, cancel the pending request first to avoid conflicting with the store reset:
import { onError } from '@apollo/client/link/error'; export default ({ app }: Context) => { const errorLink = onError(async ({ graphQLErrors, operation, forward }) => { if (graphQLErrors) { for (const err of graphQLErrors) { if (err?.extensions?.exception.status === 401) { // Mark the operation as skipped to cancel it operation.setContext({ skip: true }); // Perform logout after canceling the request await app.$apolloHelpers.onLogout(); // Redirect to login page (optional but recommended) app.router?.push('/login'); break; // Stop processing other errors to avoid duplicate logout } } } // Only forward the operation if no 401 was handled return forward(operation); }); // ... rest of your client config }
b. Avoid Async/Await in graphQLErrors.map()
The map() method is synchronous, so using async inside it creates unresolved Promises that break the link chain. Use a for...of loop instead for proper async error handling.
2. Custom Apollo Client Configuration Optimizations
Your current setup replaces the default Apollo link entirely, which can break built-in features like HTTP headers or cache integration. Here's how to refine it:
a. Preserve Default Link Functionality
Combine your error link with the default HTTP link instead of replacing it, so you don't lose core Apollo behavior:
import { createHttpLink } from '@apollo/client/core'; import { from } from '@apollo/client/link/core'; import { InMemoryCache } from '@apollo/client/core'; export default ({ app }: Context) => { // Create a standard HTTP link for GraphQL requests const httpLink = createHttpLink({ uri: 'http://localhost:3001/graphql', }); const errorLink = onError(async ({ graphQLErrors, networkError, operation, forward }) => { // ... your refined error handling logic here }); return { // Chain errorLink before httpLink to catch errors early link: from([errorLink, httpLink]), // Reuse the existing cache or initialize a new one cache: app?.$apollo?.defaultClient?.cache || new InMemoryCache(), // Keep other default Nuxt Apollo configs (like auth headers) if needed }; }
b. Add Token Refresh Logic (If Needed)
Instead of immediately logging out on 401, you can attempt to refresh the token first and retry the request. Here's a simplified implementation:
// Inside your errorLink callback if (err?.extensions?.exception.status === 401) { // Prevent concurrent refresh requests if (!app.$apolloHelpers.isRefreshing) { app.$apolloHelpers.isRefreshing = true; try { // Fetch new token using your refresh endpoint const newTokenData = await app.$axios.$post('/refresh-token', { refreshToken: app.$apolloHelpers.getRefreshToken(), }); // Update auth state with new tokens app.$apolloHelpers.onLogin(newTokenData.token, newTokenData.refreshToken); // Update the current request's authorization header operation.setContext(({ headers = {} }) => ({ headers: { ...headers, authorization: `Bearer ${newTokenData.token}`, }, })); app.$apolloHelpers.isRefreshing = false; // Retry the original request return forward(operation); } catch (refreshErr) { // Refresh failed, proceed to logout app.$apolloHelpers.isRefreshing = false; await app.$apolloHelpers.onLogout(); app.router?.push('/login'); operation.setContext({ skip: true }); } } else { // Wait for ongoing refresh to complete, then retry return new Promise((resolve) => { app.$apolloHelpers.onRefreshDone = () => { resolve(forward(operation)); }; }); } }
Note: You'll need to add isRefreshing and onRefreshDone state properties to your apolloHelpers utility to manage the refresh flow.
c. Improve Error Classification
Add specific handling for different GraphQL error codes to make debugging easier:
if (graphQLErrors) { for (const err of graphQLErrors) { switch (err.extensions?.code) { case 'UNAUTHENTICATED': // Handle 401 token expiry logic break; case 'FORBIDDEN': console.error('You don\'t have permission to perform this action'); break; case 'VALIDATION_ERROR': console.error('Input validation failed:', err.message); break; default: console.error('GraphQL Error:', err.message); } } }
d. Remove Type Casts
Avoid using errorLink as any by ensuring proper type imports. The onError function from @apollo/client/link/error already returns a valid Apollo Link type, so no casting is needed.
内容的提问来源于stack exchange,提问作者Antoni

