userQuery fetch sent before cookie set for auth

Hopefully I can provide enough context where this is clear. I have a Next.js app that is using ApolloClient. I have _app.js wrapped in the withApolloHOC component from withApollo.js file below so the whole app should have access to Apollo (Which it seems to).

The problem is that on line 21 of the withApollo.js file an auth header is set from a cookie that is set from the signed-in.js file. For whatever reason the query works on the signed-in page but NOT on any other page…so what happens is:

  1. The user signs in and is redirected to sgined-in page
  2. The signed-in page sets the cookie on line 43 setToken(result.idToken, result.accessToken);
  3. If auth was successful the query is made on line 32 const [getDbUser, { loading, error, data }] = useLazyQuery(GET_USER_BY_EMAIL); (when getDbUser is executed it works)
  4. The signed-in page then redirects to index.js
  5. A component SetUser.js in index.js tries to run the same query except it uses userQuery instead of userLazyQuery and fails because the auth header is not set to the cookie.
  6. If I refresh index.js the same userQuery that just failed to set the auth header now properly sets the auth header.

My question…or what I’m trying to figure out is why is the auth properly set at step 3 and 6 but not at step 5?

withApollo.js

import { useMemo } from "react";
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
import withApollo from "next-with-apollo";
import { ApolloProvider } from "@apollo/client";
import Cookie from 'js-cookie';
import { endpoint } from '../config';

let apolloClient;

function createApolloClient() {
  const isBrowser = typeof window !== 'undefined';
  const authCookie = (isBrowser) ? Cookie.get('idToken') : '';

  return new ApolloClient({
    connectToDevTools: isBrowser,
    ssrMode: !isBrowser,
    link: new HttpLink({
      uri: endpoint, // Server URL (must be absolute)
      credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers`
      headers: {
        authorization: authCookie
      }
    }),
    cache: new InMemoryCache(),
  });
}

export function initializeApollo(initialState = null) {
  const _apolloClient = apolloClient ?? createApolloClient();

  // 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 = _apolloClient.extract();
    // Restore the cache using the data passed from getStaticProps/getServerSideProps
    // combined with the existing cached data
    _apolloClient.cache.restore({ ...existingCache, ...initialState });
  }
  // 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 function useApollo(initialState) {
  const store = useMemo(() => initializeApollo(initialState), [initialState]);
  return store;
}


export default function withApolloHOC(WrappedComponent) {
  return (props) => {
    const apolloClient = useApollo(props.initialApolloState);
    return (
      <ApolloProvider client={apolloClient}>
        <WrappedComponent {...props} />
      </ApolloProvider>
    )
  }

}

signed-in.js

import React, { useEffect, useState } from 'react';
import Router from 'next/router';
import { useLazyQuery } from '@apollo/client';
import { setToken } from '../../lib/auth';
import { getUserFromToken } from '../../lib/auth';
import { parseHash } from '../../lib/auth0';
import { GET_USER_BY_EMAIL } from '../../sharedQueries/users';

// REDUX
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { updateCurrentUser } from '../../lib/redux/actions/userActions';

// Redirect function
function redirect(url) {
  console.log('performing redirect: ' + url)
  if (url) {
    Router.push(url)
  } else {
    Router.push('/');
  }
}

function loadingView() {
  return null;
}


function signIn({ url, updateCurrentUser }) {
  const [authUser, setAuthUser] = useState();
  const isBrowser = typeof window !== 'undefined';
  const [getDbUser, { loading, error, data }] = useLazyQuery(GET_USER_BY_EMAIL);

  // Only run in browser
  if (!isBrowser) return null;

  useEffect(() => {
    parseHash((err, result) => {
      // Need to return on error or else everything FAILS
      if (err) return null;

      // Set Tokens and auth user state
      setToken(result.idToken, result.accessToken);
      setAuthUser(getUserFromToken(result.idToken));
    });
  }, [authUser]); // Only re-run the effect changes

  if (!authUser || loading) return loadingView();

  // Fetch DB user if email verified
  if (authUser.email_verified && !error && !data) {
    getDbUser({
      variables: {
        email: authUser.email
      }
    });
  }

  const dbUser = (data) ? data.user : undefined;

  updateCurrentUser({ ...authUser, dbUser });
  redirect(url);

  return null;
}

const mapDispatchToProps = dispatch => {
  return {
    updateCurrentUser: bindActionCreators(updateCurrentUser, dispatch)
  }
}

export default connect(
  null,
  mapDispatchToProps
)(signIn);

SetUser.js

import React from 'react';
import { getUserFromLocalCookie } from '../lib/auth';
import { useQuery } from '@apollo/client';
import { GET_USER_BY_EMAIL } from '../sharedQueries/users';

// REDUX
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { updateCurrentUser } from '../lib/redux/actions/userActions';


function setUser({ updateCurrentUser }) {
  const loggedUser = (typeof window !== 'undefined')
    ? getUserFromLocalCookie()
    : null;

  if (loggedUser) {
    const { loading, error, data } = useQuery(GET_USER_BY_EMAIL, {
      variables: {
        email: loggedUser.email
      }
    });

    console.log(error) // On first run I get an error, but on refresh it works..why?
    if (loading || error) return <span></span>;
    updateCurrentUser({ ...loggedUser, dbUser: data.user });
  }

  return <span></span>;
}

const mapDispatchToProps = dispatch => {
  return {
    updateCurrentUser: bindActionCreators(updateCurrentUser, dispatch)
  }
}

export default connect(
  null,
  mapDispatchToProps
)(setUser);
1 Like

What is the error on first run? Additionally, you’re not conditionally checking for an error on that log, nor are you logging it as an error.

My guess is that if your error isn’t being thrown inside your app itself, then loggedUser.email may be nullish.

If it’s not nullish, then your query may be having issues, which would then point to your issue being server-side, not client-side.

I can confirm that loggedUser.email is not null when the query is executed. The Apollo server is returning an auth error because when querying users you have to be that specific user OR an admin. Both those permissions are determined based on the authorization header that is set inside withApollo.js. The issue is that after signed-in.js redirects the cookie storing the auth token is null? Which is weird because before the redirects and on refresh it is NOT null. I think what I’m seeing is something very similar to SSR - Send request with cookie from apollo client with Next.js · Issue #5089 · apollographql/apollo-client · GitHub.

For now my workaround until I solve the issue or the ?bug? is fixed is to set a cookie of something like “didDBUserFetchFail” to 1 if it fails and then 0 if it does not fail…then if there was an auth error and the cookie is 0 then reload the page. It is super dirty but at least it works for now.