Apollo is not requesting graphql in ssr (through next.js)

Viewed 380

Intended outcome:

I have volatile data that I want to be queried from graphql on each display (not cached)

Actual outcome:

Queries are sent to the backend graphql server in development mode (next dev), but when the project is built and run (next start), only the first query is executed for each query: it seems the lib returns data from cache for all susequent requests.

I didn't set any maxAge anywhere, so from what I understood, it should default to 0 and have all queries executed without caching.

Since things work in development but not in production, I consider this a bug, and I submitted an issue. Did anyone workaround this bug?

/apollo/client.js

import {useMemo} from 'react'
import {ApolloClient, HttpLink, gql} from '@apollo/client'
import merge from 'deepmerge'
import {cache} from './cache'
import {isSSR} from "../constants/util";
import isEqual from 'lodash/isEqual'
import logger from "../lib/logger";

export const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__'

const typeDefs = gql`
  extend type Query {
    draftComplaints: [Complaint]
  }
`;

let apolloClient

function createIsomorphLink() {
  if (typeof window === 'undefined') {
    const frontEndGraphQLUrl = process.env.NEXT_PUBLIC_FRONT_END_SSR_URL+'/api/graphql'
    logger.debug("server side next links are pointing to ", frontEndGraphQLUrl)
    return new HttpLink({
      uri: frontEndGraphQLUrl,
      headers: {ssr:true},
      credentials: 'same-origin',
    })
  } else {
    return new HttpLink({
      uri: '/api/graphql',
      headers: {ssr:false},
      credentials: 'same-origin',
    })
  }
}

function createApolloClient() {
  return new ApolloClient({
    ssrMode: typeof window === 'undefined',
    link: createIsomorphLink(),
    cache,
    typeDefs
  })
}

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
  // 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 (isSSR()) return _apolloClient
  // Create the Apollo Client once in the client
  if (!apolloClient) apolloClient = _apolloClient

  return _apolloClient
}

export function useApollo(pageProps) {
  const state = pageProps[APOLLO_STATE_PROP_NAME]
  const store = useMemo(() => initializeApollo(state), [state])
  return store
}

export function addApolloState(client, pageProps) {
  if (pageProps?.props) {
    pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract()
  }

  return pageProps
}

In my endpoint, I'm adding the connected user to the header then forwarding the request to a backend server. My issue is that in ssr, this code is not called after the first display

/api/graphql.js import {getSession} from "next-auth/client";

function isTrueStr(str) {
    return str === true ||
        (str != null &&
        str.toString().toLowerCase() === 'true')
}

/**
 * typeof window == 'undefined' doesn't work here, so client HttpLink was enriched by a header boolean field 'ssr'
 * @param req
 * @returns {Promise<{"content-type": string, "infra-token": *}>}
 */

async function prepareHeaders(req) {
    let infraToken = ""
    const ssr = isTrueStr(req.headers['ssr'])
    if (ssr == true) {
        //in SSR, we rely on client.query({context:{headers:{'infra-token':token}}})
        infraToken = req.headers['infra-token']
    } else {
        //in client rendering, we rely on the user that made the request
        const session = await getSession({req})
        infraToken = session?.infraToken
    }
    // logger.debug("api/graphql: {ssr: ", ssr, " Request token:", infraToken)
    let headers = {
        'content-type': 'application/json',
        'infra-token': infraToken ?? "",
        ssr
    }
    //On server-side, we must wrap our headers in a node-fetch Header class instance
    if (ssr) {
        const Headers = (await import('node-fetch')).Headers
        headers = new Headers(headers)
    }
    return headers;
}

export default async (_req, _res) => {
    let headers = await prepareHeaders(_req);

    const query = {
        method: 'POST', // *GET, POST, PUT, DELETE, etc.
        mode: 'cors', // no-cors, *cors, same-origin
        cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
        headers,
        redirect: 'follow', // manual, *follow, error
        body: JSON.stringify({
            operationName: _req.body.operationName,
            variables: _req.body.variables,
            query: _req.body.query
        }) // body data type must match "Content-Type" header
    };
    const graphqlBaseUrl = process.env.NEXT_PUBLIC_BACK_END_URL
    const res = await fetch(graphqlBaseUrl + '/api/graphql', query)
    const json = await res.json()

    console.log( "GRAPHQL QUERY: ", _req.body.operationName, " | variables: ", _req.body.variables )
    console.log( "GRAPHQL RESPONSE: ", JSON.stringify(json) )

    _res.statusCode = 200;
    _res.setHeader('content-type', 'application/json');
    _res.setHeader('cache-control', 'no-store, max-age=0');
    _res.json(json)
    _res.end()
};

export function prepareContext(session) {
    return {
        headers: {'infra-token': session?.infraToken ?? ""}
    };
}

What I get in the output is only:

debug:["server side next links are pointing to ","http://next_server:3000/api/graphql"]

Which is the correct endpoint (so the SSR link is correctly created, but the API is not called), seems Apollo is caching the response of the API call, which is definitely not the intended behavior for my case. The issue is that I can't find a documentation talking about next.js or apollo caching data in ssr

You might think there's an issue with my cache code, but it doesn't enter to it neither: the line

console.log("merging: existing ", existing.count, " incoming: ", incoming.count)

is not executed for the complaintConnection request

/apollo/cache.js import { InMemoryCache, makeVar } from '@apollo/client';

export const draftComplaints = makeVar([])

export const cache = new InMemoryCache({
    typePolicies: {
        Query: {
            fields: {
                draftComplaints: {
                    read() {
                        return draftComplaints();
                    }
                },
                complaintConnection: {
                    keyArgs: ['filter'],
                    merge(existing = [], incoming) {
                        console.log("merging: existing ", existing.count, " incoming: ", incoming.count)

                        if(existing?.pageInfo  == null)
                            return incoming

                        if(existing.pageInfo.endCursor == incoming.pageInfo.endCursor)
                            return existing

                        return {
                            __typename: "ComplaintConnection",
                            count: existing.count,
                            edges: [...existing.edges, ...incoming.edges],
                            pageInfo: incoming.pageInfo
                        };
                    },
                }
            }
        }
    }
});
0 Answers
Related