Nextjs 13 page router - query response is null after updating invalid access token on a server(ssr)

30 Views Asked by At

On the first page load with the invalid access token, the response from meQuery is null, for some reason, it is not able to return a proper value from a query that is executed after refreshing tokens.

1st console log - refreshing tokens; 2nd console log - result from query in getServerSideProps

This issue occurs only on a server(ssr), on the client it works fine.

lib/apollo.tsx

/* eslint-disable @typescript-eslint/no-explicit-any */
import type { FetchResult, NormalizedCacheObject } from '@apollo/client';
import {
  ApolloClient,
  ApolloLink,
  HttpLink,
  InMemoryCache,
  Observable,
} from '@apollo/client';
import { onError } from '@apollo/client/link/error';
import { WebConfig } from '@commerce/config';
import merge from 'deepmerge';
import { isEqual } from 'lodash';
import type { GetServerSidePropsContext } from 'next';
import { useMemo } from 'react';

export const APOLLO_STATE_PROPERTY_NAME = '__APOLLO_STATE__';

let apolloClient: ApolloClient<NormalizedCacheObject> | undefined;

const createApolloClient = (ctx?: GetServerSidePropsContext) => {
  const httpLink = new HttpLink({
    uri: WebConfig.apiUrl,
    credentials: 'include',
  });
  const errorLink = onError(({ graphQLErrors, operation, forward }) => {
    if (graphQLErrors) {
      for (const err of graphQLErrors) {
        switch (err.extensions?.code) {
          case 'UNAUTHENTICATED': {
            const observable = new Observable<FetchResult<Record<string, any>>>(
              (observer) => {
                (async () => {
                  try {
                    const data = await fetch(WebConfig.apiUrl, {
                      method: 'POST',
                      headers: {
                        Cookie: ctx?.req?.headers.cookie ?? '',
                        'content-Type': 'application/json',
                      },
                      body: JSON.stringify({
                        query: `
                        mutation Refresh {
                          refresh
                        }
                        `,
                      }),
                      credentials: 'include',
                    });

                    ctx?.res?.setHeader(
                      'set-cookie',
                      data.headers.getSetCookie(),
                    );
                    console.log(ctx?.res?.getHeaders());

                    const subscriber = {
                      next: observer.next.bind(observer),
                      error: observer.error.bind(observer),
                      complete: observer.complete.bind(observer),
                    };
                    forward(operation).subscribe(subscriber);
                  } catch (error) {
                    observer.error(error);
                  }
                })();
              },
            );

            return observable;
          }
        }
      }
    }
  });

  const authLink = new ApolloLink((operation, forward) => {
    operation.setContext(({ headers = {} }) => ({
      headers: {
        ...headers,
        Cookie: ctx?.req?.headers.cookie ?? '',
      },
    }));
    return forward(operation);
  });

  return new ApolloClient({
    ssrMode: typeof window === 'undefined',
    ssrForceFetchDelay: 5000,
    link: ApolloLink.from([errorLink, authLink, httpLink]),
    cache: new InMemoryCache({}),
    credentials: 'include',
    defaultOptions: {
      query: {
        errorPolicy: 'all',
        notifyOnNetworkStatusChange: true,
      },
      mutate: {
        errorPolicy: 'all',
      },
    },
  });
};

export function initializeApollo(
  initialState: NormalizedCacheObject | null = null,
  ctx: any = undefined,
) {
  const client = apolloClient ?? createApolloClient(ctx);

  // If your page has Next.js data fetching methods that use Apollo Client,
  // the initial state gets hydrated here
  if (initialState) {
    // Get existing cache, loaded during client side data fetching
    const existingCache = client.extract();

    // Merge the existing cache into data passed from getStaticProps/getServerSideProps
    const data = merge(initialState, existingCache, {
      // combine arrays using object equality (like in sets)
      arrayMerge: (destinationArray, sourceArray) => [
        ...sourceArray,
        ...destinationArray.filter((d) =>
          sourceArray.every((s) => !isEqual(d, s)),
        ),
      ],
    });

    // Restore the cache with the merged data
    client.cache.restore(data);
  }

  // For SSG and SSR always create a new Apollo Client
  if (typeof window === 'undefined') {
    return client;
  }

  // Create the Apollo Client once in the client
  if (!apolloClient) {
    apolloClient = client;
  }

  return client;
}

export function addApolloState(
  client: ApolloClient<NormalizedCacheObject>,
  pageProps: any,
) {
  if (pageProps?.props) {
    pageProps.props[APOLLO_STATE_PROPERTY_NAME] = client.cache.extract();
  }

  return pageProps;
}

export const useApollo = (pageProps: any) => {
  const state = pageProps[APOLLO_STATE_PROPERTY_NAME];
  const store = useMemo(() => initializeApollo(state), [state]);
  return store;
};

pages/index.tsx

import { addApolloState, initializeApollo } from '@commerce/web/apollo';
import { MeDocument, MeQuery, useMeQuery } from '@commerce/web/generated';
import { GetServerSideProps } from 'next';

export const getServerSideProps: GetServerSideProps = async (ctx) => {
  const apolloClient = initializeApollo(null, ctx);

  const { data } = await apolloClient.query<MeQuery>({
    query: MeDocument,
  });

  console.log(data);

  return addApolloState(apolloClient, {
    props: {},
  });
};

export function Index() {
  const { data } = useMeQuery();

  return <div>{JSON.stringify(data)}</div>;
}

export default Index;

I tried to investigate what was wrong and, unfortunately, didn't manage to find a solution

"@apollo/client": "^3.8.10",
"next": "13.4.4",
0

There are 0 best solutions below