2

I've been looking at this for days, and simply can't figure this out!

My apolloClient is set up nearly identical to the one found in this article. Rather than add the token stored in the browser cookies for every single request I make throughout my app, I'd like to set it during initialization of ApolloClient.

What I'm seeing however, is a graphQL error being sent from my server telling me that I haven't supplied a JWT, and when I look at the headers for the request, Authorization is blank. Ultimately, I need this to be able to work for both server- and client-side queries and mutations.

pages/_app.tsx

// third party
import type { AppProps } from 'next/app';
import Head from 'next/head';
import { ApolloProvider } from '@apollo/client';
import { MuiThemeProvider, StylesProvider } from '@material-ui/core/styles';
import CssBaseline from '@material-ui/core/CssBaseline';
import { ThemeProvider } from 'styled-components';

// custom
import Page from 'components/Page';
import theme from 'styles/theme';
import GlobalStyle from 'styles/globals';
import { useApollo } from 'lib/apolloClient';

function App({ Component, pageProps, router }: AppProps): JSX.Element {
  const client = useApollo(pageProps);

  return (
    <>
      <Head>
        <title>My App</title>
      </Head>
      <ApolloProvider client={client}>
        <MuiThemeProvider theme={theme}>
          <ThemeProvider theme={theme}>
            <StylesProvider>
              <CssBaseline />
              <GlobalStyle />
              <Page>
                <Component {...pageProps} />
              </Page>
            </StylesProvider>
          </ThemeProvider>
        </MuiThemeProvider>
      </ApolloProvider>
    </>
  );
}

export default App;

pages/index.tsx

/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { useEffect, useState } from 'react';
import Head from 'next/head';
import { GetServerSidePropsContext, NextPageContext } from 'next';
import { Box, Grid, Typography } from '@material-ui/core';
import Widget from 'components/common/Widget';
import Loading from 'components/common/Loading';
import { addApolloState, initializeApollo } from 'lib/apolloClient';
import { MeDocument } from 'generated/types';

const Home: React.FC = () => {
  return (
    <>
      <Head>
        <title>Dashboard</title>
      </Head>
      <Grid container spacing={3}>
        ... other rendered components
      </Grid>
    </>
  );
};

export const getServerSideProps = async (
  context: GetServerSidePropsContext
) => {
  const client = initializeApollo({ headers: context?.req?.headers });

  try {
    await client.query({ query: MeDocument });
    return addApolloState(client, {
      props: {},
    });
  } catch (err) {
    // return redirectToLogin(true);
    console.error(err);
    return { props: {} };
  }
};

export default Home;

lib/apolloClient.ts

import { useMemo } from 'react';
import {
  ApolloClient,
  ApolloLink,
  InMemoryCache,
  NormalizedCacheObject,
} from '@apollo/client';
import { onError } from '@apollo/link-error';
import { createUploadLink } from 'apollo-upload-client';
import merge from 'deepmerge';
import { IncomingHttpHeaders } from 'http';
import fetch from 'isomorphic-unfetch';
import isEqual from 'lodash/isEqual';
import type { AppProps } from 'next/app';
import getConfig from 'next/config';

const { publicRuntimeConfig } = getConfig();

const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__';

let apolloClient: ApolloClient<NormalizedCacheObject> | undefined;

const createApolloClient = (headers: IncomingHttpHeaders | null = null) => {
  console.log('createApolloClient headers: ', headers);
  // isomorphic fetch for passing the cookies along with each GraphQL request
  const enhancedFetch = async (url: RequestInfo, init: RequestInit) => {
    console.log('enhancedFetch headers: ', init.headers);
    const response = await fetch(url, {
      ...init,
      headers: {
        ...init.headers,
        'Access-Control-Allow-Origin': '*',
        // here we pass the cookie along for each request
        Cookie: headers?.cookie ?? '',
      },
    });
    return response;
  };

  return new ApolloClient({
    // SSR only for Node.js
    ssrMode: typeof window === 'undefined',
    link: ApolloLink.from([
      onError(({ graphQLErrors, networkError }) => {
        if (graphQLErrors)
          graphQLErrors.forEach(({ message, locations, path }) =>
            console.log(
              `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
            )
          );
        if (networkError)
          console.log(
            `[Network error]: ${networkError}. Backend is unreachable. Is it running?`
          );
      }),
      // this uses apollo-link-http under the hood, so all the options here come from that package
      createUploadLink({
        uri: publicRuntimeConfig.graphqlEndpoint,
        // Make sure that CORS and cookies work
        fetchOptions: {
          mode: 'cors',
        },
        credentials: 'include',
        fetch: enhancedFetch,
      }),
    ]),
    cache: new InMemoryCache(),
  });
};

type InitialState = NormalizedCacheObject | undefined;
type ReturnType = ApolloClient<NormalizedCacheObject>;

interface IInitializeApollo {
  headers?: IncomingHttpHeaders | null;
  initialState?: InitialState | null;
}

export const initializeApollo = (
  { headers, initialState }: IInitializeApollo = {
    headers: null,
    initialState: null,
  }
): ReturnType => {
  console.log('initializeApollo headers: ', headers);
  const _apolloClient = apolloClient ?? createApolloClient(headers);

  // If your page has Next.js data fetching methods that use Apollo Client, the initial state
  // get hydrated here
  if (initialState) {
    // Get existing cache, loaded during client side data fetching
    const existingCache = _apolloClient.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
    _apolloClient.cache.restore(data);
  }

  // For SSG and SSR always create a new Apollo Client
  if (typeof window === 'undefined') return _apolloClient;
  // Create the Apollo Client once in the client
  if (!apolloClient) apolloClient = _apolloClient;

  return _apolloClient;
};

export const addApolloState = (
  client: ApolloClient<NormalizedCacheObject>,
  pageProps: AppProps['pageProps']
): ReturnType => {
  if (pageProps?.props) {
    pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract();
  }

  return pageProps;
};

export function useApollo(pageProps: AppProps['pageProps']): ReturnType {
  const state = pageProps[APOLLO_STATE_PROP_NAME];
  const store = useMemo(
    () => initializeApollo({ initialState: state }),
    [state]
  );
  return store;
}

I've got three console logs inside of the apolloClient.ts file. When I reload the page, the initializeApollo headers and createApolloClient headers console logs both return full (and correct) headers, including the cookie I'm after. enhancedFetch headers ONLY has { accept: '*/*', 'content-type': 'application/json'}. So that's the first oddity. I then receive my server error about a missing JWT, and all three console logs are then run twice more: initializeApollo headers is undefined, createApolloClient headers is null, and enhancedFetch headers remains the same.

My gut tells me that it's something to do with the server not receiving the header on its first go, so it messes up the following console logs (which I suppose could be client-side?). I'm kind of at a loss at this point. What am I missing??

J. Jackson
  • 3,326
  • 8
  • 34
  • 74

0 Answers0