question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Apollo is not requesting graphql in ssr (through next.js) : requests only for the first query

See original GitHub issue

Intended outcome:

I have volatile data that I want to be refreshed on each display

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.

How to reproduce the issue:

Here’s my configuration code for the apollo client:

/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
                        };
                    },
                }
            }
        }
    }
});

Versions

System: OS: Linux 5.11 Alpine Linux Binaries: Node: 14.17.3 - /usr/local/bin/node Yarn: 1.22.5 - /usr/local/bin/yarn npm: 6.14.13 - /usr/local/bin/npm npmPackages: @apollo/client: ^3.4.17 => 3.4.17 apollo-server-micro: ^3.5.0 => 3.5.0

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:2
  • Comments:9 (1 by maintainers)

github_iconTop GitHub Comments

2reactions
mlarchercommented, Jun 14, 2022

I can confirm the bug. For now, disabling the cache works for me as well, but I’m not sure how to act when moving to production.

we use fetchPolicy: isBrowser() ? 'network-only' : 'no-cache',, that should be enough as server don’t need cache at all afaict

2reactions
ziedHamdicommented, Mar 4, 2022

So I managed to fix the ssr bug by initiating my client with the following options:

  defaultOptions = {
    query: {
      fetchPolicy: 'no-cache',
      errorPolicy: 'all',
    }
  }

Now, refreshing the page (ssr rendering), works.

But I still have an issue: when I rerender a page (client side), it doesn’t query the graphql endpoint.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Server-side rendering - Apollo GraphQL Docs
Server-side rendering (SSR) is a performance optimization for modern web apps. It enables you to render your app's initial state to raw HTML...
Read more >
SSR crashing in Next.js on unsuccessful GraphQL request ...
I created this repo following the guide and put the same apollo client as yours except for the pusher link. It seems it...
Read more >
Using Apollo Client for Next.js SSR and CSR with TypeScript
We setup Apollo Client to make requests to our GraphQL API. I also show you how to make your requests faster by connecting...
Read more >
Server-side rendering (SSR) using Apollo and Next.js - Medium
I'm initializing the apollo client at the very first line since on the server-side it is better to create a new client for...
Read more >
How to Fetch GraphQL Data in Next.js with Apollo GraphQL
Creates a new GraphQL query inside of the gql tag · Creates a new query request using client. · It uses await to...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found