Apollo-client: Initial Server Side Rendering Fails, Tries To Make Request to Current Server

Created on 30 Nov 2018  Â·  16Comments  Â·  Source: apollographql/apollo-client

Intended outcome:

I'm building a NextJS app that uses apollo-client to fetch data during Server-side rendering, with credentials included.

Actual outcome:

Any time I try to include CORS credentials to include cookies, the initial request will fail on the server side.

On initial load, you see that several components start in the "loading" state, even though they're supposed to be rendered server-side. They do eventually populate on the client, but in my logs I'm getting this error:

[Network error]: SyntaxError: Unexpected token N in JSON at position 0

Which I know means that I'm getting HTML or an error back when apollo-client expects JSON.

But the reason this is happening is because it looks like apollo-client is trying to make a request to the same server that I'm doing SSR on, not the endpoint that I've specified in my config:

at=info method=POST path="/" host=advanced-react-frontend.herokuapp.com request_id=d406a7fe-4fc4-4dc0-89fd-9ab9076b1608 fwd="71.78.33.134,54.147.22.164" dyno=web.1 connect=0ms service=4ms status=501 bytes=123 protocol=https

Here's a snippet of my config as well:

https://github.com/mattfwood/next-apollo-issue/blob/master/lib/withData.js

 return new ApolloClient({
    uri: 'https://advanced-react-backend.herokuapp.com',
    request: operation => {
      operation.setContext({
        fetchOptions: {
          credentials: 'include',
        },
        headers,
      });
    },
});

How to reproduce the issue:

Here's a live demo of the issue:
https://advanced-react-frontend.herokuapp.com/

And here's the repo:
https://github.com/mattfwood/next-apollo-issue

I've been trying to fix this for more than 10 hours. Here are the things I've identified and that made it so difficult to debug:

  • This only happens when my frontend and backend are both on real servers. When I run my frontend locally, it always successfully queries my (local) server while doing server-side rendering.
  • This made me think that it might've been a CORS issue, but I've triple checked and I am getting no request to my backend server before the page initially renders. The issue is because apollo-client is trying to query the _current_ server (my NextJS server) first, which doesn't have a route for a POST to /, so it returns a 501. In fact, if I use apollo-link-error to force Apollo to throw an error if a query fails, I get absolute no requests logged on my server.
  • If I turn CORS completely off on both my frontend and backend, it works as intended. But obviously since I'm trying to use cookies to store user session, this isn't an option.
  • I've also tried copying this example apollo config exactly, and it still has the same issues.

Any help would be greatly appreciated. The entire apollo library is incredibly well-designed and I've been trying to dig into the source code to figure out why this might happen, but so far I haven't had any luck.

Versions

System:
OS: macOS 10.14.1
Binaries:
Node: 11.1.0 - /usr/local/bin/node
Yarn: 1.12.1 - /usr/local/bin/yarn
npm: 6.4.1 - /usr/local/bin/npm
Browsers:
Chrome: 70.0.3538.110
Safari: 12.0.1
npmPackages:
apollo-boost: ^0.1.16 => 0.1.22
apollo-client: ^2.4.2 => 2.4.7
next-with-apollo: ^3.1.3 => 3.3.0
react-apollo: ^2.2.1 => 2.3.2

Most helpful comment

@mattfwood I have been having this exact same problem (I am also building a nextjs app with SSR and apollo-client and when it is deployed it doesn't work). Even with JWT auth. I have looked through your code to try and help my problem, but then I realized something that we both overlooked!

You are right, this part is the problem.

request: operation => {
      operation.setContext({
        fetchOptions: {
          credentials: 'include',
        },
        headers,
      });
    },

More specifically the problem is headers. I, like you, have function createClient({ headers }) {***code that returns the apollo client***}. The headers are then set in the request method when the apollo client is run. If you console.log out headers and watch logs on the deployed app, you get back a big long object. One of them being host: 'your-front-end-host.com'. The request setContext overwrites the host of your back-end graphql uri you set earlier. Essentially, you end up replacing your graphql endpoint with your front-end app endpoint. It took me forever to find it because I wasn't even thinking about ES6 and how it makes headers, into headers: headers. Not a problem on localhost because the host would be the same. So, my app works now that I am only pulling out of headers what I need instead of replacing it entirely. For example, this is what I have now:

request: operation => {
      operation.setContext({
        fetchOptions: {
          credentials: 'include',
        },
        headers: {
           cookie: headers && headers.cookie // NOTE: client-side headers is undefined!
        },
      });
    },

Anyway, I have been banging my head and had the exact same problem as you. Figured I would share a little bit of what I learned in hopes it will help you with your application! Good luck!

All 16 comments

Okay, I've narrowed it down to this:

request: operation => {
      operation.setContext({
        fetchOptions: {
          credentials: 'include',
        },
        headers,
      });
    },

When using apollo-boost or even if I'm constructing a custom client with apollo-client directly, if I include these lines, it tries to make a request on load but it looks like it isn't passing the cookies when server-rendering.

If I don't include the request property:

new ApolloClient({
    uri: 'https://task-management-backend.herokuapp.com',
    fetchOptions: {
      credentials: 'include',
    },
    credentials: 'include',
  })
})

I'm able to sign in and get information that requires credentials, but when I reload the page, the cookies are cleared / lost.

It's still unclear to me why including the "request" operation would cause apollo-client to make a post to the current (NextJS) server.

This was a nightmare bug I fought with for several days.

In the end I decided to change my entire authentications flow to be JWT-based instead of cookies / credentials based.

Removing CORS allowed it to work on initial server side render. This doesn’t exactly fix the bug itself (I have a suspicion I had to configure CORS using a custom NextJS server), but it’s probably a better option in general and easier to use if I end up using my backend for a mobile app as well.

@mattfwood I have been having this exact same problem (I am also building a nextjs app with SSR and apollo-client and when it is deployed it doesn't work). Even with JWT auth. I have looked through your code to try and help my problem, but then I realized something that we both overlooked!

You are right, this part is the problem.

request: operation => {
      operation.setContext({
        fetchOptions: {
          credentials: 'include',
        },
        headers,
      });
    },

More specifically the problem is headers. I, like you, have function createClient({ headers }) {***code that returns the apollo client***}. The headers are then set in the request method when the apollo client is run. If you console.log out headers and watch logs on the deployed app, you get back a big long object. One of them being host: 'your-front-end-host.com'. The request setContext overwrites the host of your back-end graphql uri you set earlier. Essentially, you end up replacing your graphql endpoint with your front-end app endpoint. It took me forever to find it because I wasn't even thinking about ES6 and how it makes headers, into headers: headers. Not a problem on localhost because the host would be the same. So, my app works now that I am only pulling out of headers what I need instead of replacing it entirely. For example, this is what I have now:

request: operation => {
      operation.setContext({
        fetchOptions: {
          credentials: 'include',
        },
        headers: {
           cookie: headers && headers.cookie // NOTE: client-side headers is undefined!
        },
      });
    },

Anyway, I have been banging my head and had the exact same problem as you. Figured I would share a little bit of what I learned in hopes it will help you with your application! Good luck!

Even when I have my code as the following for the same piece of code I am getting the same logout error. Everything is working fine on the local dev environment, but the deploy is not passing my headers. This is what I currently have and I am beating myself over the head trying to figure out this refresh issue.

import withApollo from 'next-with-apollo';
import ApolloClient from 'apollo-boost';
import { endpoint, prodEndpoint } from '../config';
import { LOCAL_STATE_QUERY } from '../components/Cart';

function createClient({ headers }) {
  return new ApolloClient({
    uri: process.env.NODE_ENV === 'development' ? endpoint : prodEndpoint,
    request: operation => {
      operation.setContext({
        fetchOptions: {
          credentials: 'include',
        },
        headers: {
          cookie: headers && headers.cookie,
        },
      });
    },
    // local data
    clientState: {
      resolvers: {
        Mutation: {
          toggleCart(_, variables, { cache }) {
            // read the cartOpen value from the cache
            const { cartOpen } = cache.readQuery({
              query: LOCAL_STATE_QUERY,
            });
            // Write the cart State to the opposite
            const data = {
              data: { cartOpen: !cartOpen },
            };
            cache.writeData(data);
            return data;
          },
        },
      },
      defaults: {
        cartOpen: false,
      },
    },
  });
}

export default withApollo(createClient);

@dggodfrey After trying basically everything, I accidentally stumbled on the same solution by only specifying one header (the token / cookie). It makes perfect sense now that applying the NextJS headers would override the ones that are meant to go to the backend. I appreciate that insight, it might let me sleep at night after everything I dealt with trying to fix this, haha.

@theranbrig From that snippet (I'm also doing the Wes Bos course, as you might've guessed, haha), it looks like everything should be correct. Is there any chance you don't have the frontend URL set as an environment variable for the backend in the deployed version? That tripped me up for a bit, but taking a look at this, I don't think you should be having the same issue @dggodfrey and I were dealing with.

@theranbrig did you happen to figure out the refresh issue? I'm doing the Wes Bos coursetoo, trying to track down an answer. Is it perhaps related to cross-domain cookies?

although @dggodfrey solution worked for me, I now notice that session is not persisted, somehow the token is not being set in the cookie.

Any further insight into this issue? Seems like this issue is also relevant: https://github.com/apollographql/apollo-client/issues/4190

although @dggodfrey solution worked for me, I now notice that session is not persisted, somehow the token is not being set in the cookie.

Did you find a solution to this?

No sir, I'm sorry. @Stenbaek

I ended up tacking on a poll interval onto the query function that I needed. Fixed it for what I needed it for, but far from the best solution.

I ended up tacking on a poll interval onto the query function that I needed. Fixed it for what I needed it for, but far from the best solution.

Do you mind elaborating or do you have an example?

After some research, I think I found a solution. It all comes down to how the browser handles 3rd party cookies. Apps cannot set cookies for *.herokuapp.com.. Read more here: https://devcenter.heroku.com/articles/cookies-and-herokuapp-com

When I set up my backend on backend.mydomain.com, and the frontend on the same domain (eg. staging.mydomain.com), it works flawlessly, while still using

headers: { cookie: headers && headers.cookie },

as suggested by @dggodfrey:

For anyone struggling with cookies being wiped out on refresh, initial load after deploying to production. You have to attach a domain option to your mutation in res.cookie like:
{ domain: '.yourdomain.com' }
Then will the cookies work on refresh if your frontend is at frontend.yourdomain.com and backend at backend.yourdomain.com. It worked on localhost without this additional setting, because it's the same domain for backend and frontend: localhost. I lost a month on this only thinking it was Apollo Client issue.

I have to say I was struggling with this issue for awhile and because I stumbled across this thread I was able to fix it. Thanks so much everyone here. Especially @dggodfrey ! Much appreciated for the solution bud.

it's still giving me the same cors error

Was this page helpful?
0 / 5 - 0 ratings