apollographql / apollo-client-nextjs

Apollo Client support for the Next.js App Router
https://www.npmjs.com/package/@apollo/experimental-nextjs-app-support
MIT License
351 stars 25 forks source link

Why is observer.error not handled in getClient or useSuspenseQuery? #259

Open Algrus8 opened 1 month ago

Algrus8 commented 1 month ago

Hello! I have this code:

import { onError } from '@apollo/client/link/error';
import type { FetchResult } from '@apollo/client/link/core/types';
import {
  type ApolloClient,
  type NormalizedCacheObject,
  Observable,
} from '@apollo/client';
import { type NextSSRApolloClient } from '@apollo/experimental-nextjs-app-support/ssr';
import {
  RefreshTokensDocument,
  type RefreshTokensMutation,
} from './refreshTokens.generated';

export const createAuthErrorLink = (
  getClient:
    | (() => ApolloClient<unknown>)
    | (() => NextSSRApolloClient<NormalizedCacheObject>)
) =>
  onError(({ graphQLErrors, networkError, operation, forward }) => {
    const isAuthError = graphQLErrors?.find(
      (error) => error.extensions.code === 'UNAUTHENTICATED'
    );

    if (isAuthError) {
      // Ignore 401 error for a refresh request.
      if (operation.operationName === 'RefreshTokens') return;
      const observable = new Observable<FetchResult>((observer) => {
        const refreshTokens = async () => {
          try {
            const { data } = await getClient().mutate<RefreshTokensMutation>({
              mutation: RefreshTokensDocument,
            });

            if (!data?.refreshTokens) {
              throw new Error('Refresh Tokens Error');
            }

            const { accessToken, refreshToken } = data.refreshTokens;

            const cookie = [
              `accessToken=${accessToken};`,
              `refreshToken=${refreshToken};`,
            ];

            const oldHeaders = operation.getContext().headers as object;

            operation.setContext({
              headers: {
                ...oldHeaders,
                cookie,
              },
            });

            forward(operation).subscribe(observer);
          } catch (error) {
            observer.error(error);
          }
        };
        void refreshTokens();
      });

      return observable;
    }

    if (networkError) console.error(`[Network error]: ${networkError}`);
  });

This is my getClient definition:

import { registerApolloClient } from '@apollo/experimental-nextjs-app-support/rsc';
import {
  ApolloLink,
  HttpLink,
  from,
  InMemoryCache,
  ApolloClient,
} from '@apollo/client';
import { cookies } from 'next/headers';
import { createAuthErrorLink } from './create-auth-error-link';

const cookieLink = new ApolloLink((operation, forward) => {
  const cookieHeader = cookies();
  const oldHeaders = operation.getContext().headers as object;

  operation.setContext({
    headers: {
      ...oldHeaders,
      cookie: cookieHeader,
    },
  });

  return forward(operation);
});

export const { getClient } = registerApolloClient(() => {
  const httpLink = new HttpLink({
    uri: 'http://127.0.0.1:3000/graphql',
    credentials: 'include',
  });
  const authErrorLink = createAuthErrorLink(getClient);

  return new ApolloClient({
    cache: new InMemoryCache(),
    link: from([cookieLink, authErrorLink, httpLink]),
  });
});

And when I use useQuery from '@apollo/client', everything works correctly. However, when I use getClient or useSuspenseQuery, for example:

const { data, error } = await getClient().query<GetCurrentUserQuery>({
  query: GetCurrentUserDocument,
});

I get an unhandled error in Next.js when calling observer.error(error);, and I can't retrieve the error just from the getClient result. How can I fix that?

phryneas commented 3 weeks ago

In the useSuspenseQuery case, you should use an error boundary - and I believe in the await getClient().query case you can either use a try..catch block, or also an error boundary.

Algrus8 commented 3 weeks ago

Yes, I can handle this error with a try-catch, but can I do something to get it just from this error const { data, error } = await getClient().query?

phryneas commented 3 weeks ago

You could set the errorPolicy in your query call options - the default is none, which will throw, but you could also set errorPolicy: "all" which would make it accessible the way you want to here.