apollographql / apollo-client

:rocket:  A fully-featured, production ready caching GraphQL client for every UI framework and GraphQL server.
https://apollographql.com/client
MIT License
19.33k stars 2.65k forks source link

SSR - Send request with cookie from apollo client with Next.js #5089

Closed saostad closed 1 year ago

saostad commented 5 years ago

please help, I tried all the examples and also other tutorials on the internet but no luck to fix the problem. I am using apollo client to get the data and I am using a cookie or Bearer (which one works) for authentication but in the first request and after every refresh, it is sending request with no cookie or Bearer in it, and my API rejects the request but in the second request which it happen immediately (automatically by app NOT user) after the first one it includes cookie and Bearer on it.

I think:

I tried to force the component to render just in client-side but no luck on it here is init-apollo.tsx file:

import { ApolloClient, InMemoryCache } from "apollo-boost";
import fetch from "isomorphic-unfetch";
import { setContext } from "apollo-link-context";
import { createHttpLink } from "apollo-link-http";
import { variables } from "./variables";
import { auth } from "./auth";
import { isBrowser } from "./utils";

let apolloClient = null;

function create(initialState, { fetchOptions }) {
  const httpLink = createHttpLink({
    uri: variables.apiUri,
    credentials: "include",
    fetch: isBrowser ? fetch : undefined,
    fetchOptions
  });

  const authLink = setContext((_, { headers }) => {
    const token = auth.getUserToken();
    return {
      headers: {
        ...headers,
        authorization: token ? `Bearer ${token}` : ""
      }
    };
  });

  return new ApolloClient({
    connectToDevTools: isBrowser,
    ssrMode: false,
    link: authLink.concat(httpLink),
    cache: new InMemoryCache().restore(initialState || {}),
    ssrForceFetchDelay: 100
  });
}

here is my withApollo.tsx code:

import React from "react";
import cookie from "cookie";
import initApollo from "./init-apollo";
import Head from "next/head";
import { getDataFromTree } from "react-apollo";

function parseCookies(req, options = {}) {
  return cookie.parse(
    req ? req.headers.cookie || "" : document.cookie,
    options
  );
}

export default App => {
  return class WithApollo extends React.Component {
    public constructor(props) {
      super(props);
      this.apolloClient = initApollo(props.apolloState, {
        getToken: () => {
          return parseCookies(undefined).token;
        }
      });
    }
    public apolloClient = undefined;
    public static displayName = "withApollo(App)";

    public static async getInitialProps(ctx) {
      const {
        Component,
        router,
        ctx: { req, res }
      } = ctx;

      const apollo = initApollo(
        {},
        {
          getToken: () => parseCookies(req).token
        }
      );

      ctx.ctx.apolloClient = apollo;

      let appProps = {};
      if (App.getInitialProps) {
        appProps = await App.getInitialProps(ctx);
      }

      if (res && res.finished) {
        // When redirecting, the response is finished.
        // No point in continuing to render
        return {};
      }

      if (typeof window === "undefined") {
        try {
          // Run all GraphQL queries
          await getDataFromTree(
            <App
              {...appProps}
              Component={Component}
              router={router}
              apolloClient={apollo}
            />
          );
        } catch (error) {
          // Prevent Apollo Client GraphQL errors from crashing SSR.
          // Handle them in components via the data.error prop:
          // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
          console.error("Error while running `getDataFromTree`", error);
        }

        // getDataFromTree does not call componentWillUnmount
        // head side effect therefore need to be cleared manually
        Head.rewind();
      }

      // Extract query data from the Apollo store
      const apolloState = apollo.cache.extract();

      return {
        ...appProps,
        apolloState
      };
    }

    public render() {
      return <App {...this.props} apolloClient={this.apolloClient} />;
    }
  };
};

and _app.tsx file:

import React from "react";
import App, { Container } from "next/app";

import { runWithAdal } from "react-adal";
import { authContext } from "../helpers/adal-config";
import AppLayout from "../components/common/AppLayout";
import GlobalContext from "../helpers/global-context";
import { auth } from "../helpers/auth";
import withApollo from "../helpers/with-apollo";
import { ApolloProvider } from "react-apollo";

class MyApp extends App {
  public state = {
    isLoggedIn: auth.getUserProfile() ? true : false,
    userProfile: auth.getUserProfile(),
    logout: () => {
      auth.logOut();
    }
  };

  public static async getInitialProps({ Component, ctx }) {
    let pageProps = {};

    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }

    return { pageProps };
  }
  public render() {
    // @ts-ignore
    const { Component, pageProps, apolloClient } = this.props;

    return (
      <Container>
        <ApolloProvider client={apolloClient}>
          <GlobalContext.Provider value={this.state}>
            <AppLayout>
              <Component {...pageProps} />
            </AppLayout>
          </GlobalContext.Provider>
        </ApolloProvider>
      </Container>
    );
  }
}

/** @false: Login when app loads
 * @true: do not login when app loads (wait for other components to ask for login)
 */
const doNotLogin = false;
runWithAdal(
  authContext,
  () => {
    return MyApp;
  },
  doNotLogin
);

export default withApollo(MyApp);

My Component:

import { Query } from "react-apollo";
import gql from "graphql-tag";
import Loading from "./common/Loading";
import Error from "./common/Error";
import { isBrowser } from "../helpers/utils";

const helloQuery = gql`
  query hello {
    hello
  }
`;

const PostList = function PostList() {
  return (
    <Query query={helloQuery} ssr={false}>
      {({ loading, error, data }) => {
        if (error) {
          return <Error />;
        }
        if (loading) {
          return <Loading />;
        }

        return <section>{data.hello as string}</section>;
      }}
    </Query>
  );
};
export default PostList;
mzygmunt commented 4 years ago

Here's my modification of the official nextjs example, which passing the cookie from nextjs ctx to fetch on ssr.

import React from "react"
import Head from "next/head"
import { ApolloProvider } from "@apollo/react-hooks"
import { ApolloClient } from "apollo-client"
import { InMemoryCache } from "apollo-cache-inmemory"
import { HttpLink } from "apollo-link-http"
import fetch from "isomorphic-unfetch"

let apolloClient = null

/**
 * Creates and provides the apolloContext
 * to a next.js PageTree. Use it by wrapping
 * your PageComponent via HOC pattern.
 * @param {Function|Class} PageComponent
 * @param {Object} [config]
 * @param {Boolean} [config.ssr=true]
 */
export function withApollo(PageComponent, { ssr = true } = {}) {
    const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
        const client = apolloClient || initApolloClient(apolloState)

        return (
            <ApolloProvider client={client} >
                <PageComponent {...pageProps} />
            </ApolloProvider>
        )
    }

    // Set the correct displayName in development
    if (process.env.NODE_ENV !== "production") {
        const displayName =
            PageComponent.displayName || PageComponent.name || "Component"

        if (displayName === "App") {
            console.warn("This withApollo HOC only works with PageComponents.")
        }

        WithApollo.displayName = `withApollo(${displayName})`
    }

    if (ssr || PageComponent.getInitialProps) {
        WithApollo.getInitialProps = async ctx => {
            const { AppTree } = ctx

            // Initialize ApolloClient, add it to the ctx object so
            // we can use it in `PageComponent.getInitialProp`.
            const apolloClient = (ctx.apolloClient = initApolloClient({}, ctx.req.headers.cookie))

            // Run wrapped getInitialProps methods
            let pageProps = {}
            if (PageComponent.getInitialProps) {
                pageProps = await PageComponent.getInitialProps(ctx)
            }

            // Only on the server:
            if (typeof window === "undefined") {
                // When redirecting, the response is finished.
                // No point in continuing to render
                if (ctx.res && ctx.res.finished) {
                    return pageProps
                }

                // Only if ssr is enabled
                if (ssr) {
                    try {
                        // Run all GraphQL queries
                        const { getDataFromTree } = await import("@apollo/react-ssr")
                        await getDataFromTree(
                            <AppTree
                                pageProps={{
                                    ...pageProps,
                                    apolloClient
                                }}
                            />
                        )
                    } catch (error) {
                        // Prevent Apollo Client GraphQL errors from crashing SSR.
                        // Handle them in components via the data.error prop:
                        // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
                        console.error("Error while running `getDataFromTree`", error)
                    }

                    // getDataFromTree does not call componentWillUnmount
                    // head side effect therefore need to be cleared manually
                    Head.rewind()
                }
            }

            // Extract query data from the Apollo store
            // @ts-ignore
            const apolloState = apolloClient.cache.extract()

            return {
                ...pageProps,
                apolloState
            }
        }
    }

    return WithApollo
}

/**
 * Always creates a new apollo client on the server
 * Creates or reuses apollo client in the browser.
 * @param  {Object} initialState
 */
function initApolloClient(initialState = {}, cookie = "") {
    // Make sure to create a new client for every server-side request so that data
    // isn"t shared between connections (which would be bad)

    if (typeof window === "undefined") {
        return createApolloClient(initialState, cookie)
    }

    // Reuse client on the client-side
    if (!apolloClient) {
        // @ts-ignore
        apolloClient = createApolloClient(initialState)
    }

    return apolloClient
}

/**
 * Creates and configures the ApolloClient
 * @param  {Object} [initialState={}]
 */
function createApolloClient(initialState = {}, cookie) {
    const enchancedFetch = (url, init) => {
        return fetch(url, {
            ...init,
            headers: {
                ...init.headers,
                "Cookie": cookie
            }
        }).then(response => response)
    }

    // Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient
    return new ApolloClient({
        ssrMode: typeof window === "undefined", // Disables forceFetch on the server (so queries are only run once)
        link: new HttpLink({
            uri: "http://localhost:3000/api", // Server URL (must be absolute)
            credentials: "same-origin", // Additional fetch() options like `credentials` or `headers`
            fetch: enchancedFetch
        }),
        cache: new InMemoryCache().restore(initialState)
    })
}
juicycleff commented 4 years ago

I had the same issue with Next.Js. Tried to resolve it and no way, started feeling something was wrong with apollo, till I tried it against FusionJs and it just works

saostad commented 4 years ago

my problem was because of the bad configuration of CORS on my server. after I configured it correctly it works fine. My guess is the first request is from browser and that's why it does not include any authentication header info.

sanojsilva commented 4 years ago

Hey did you manage to fix this problem? I'm having the same issue. I'm using graphql yoga as my server. This works fine in localhost but in production the cookie is not being sent in ssr. btw I have Configured SSL in my server maybe that has something to do with this.

saostad commented 4 years ago

I added Cors in my server configuration

chemicalkosek commented 4 years ago

Can you specify what exact Cors settings on the server did you set up? I'm having the same exact issue for over a month. Works in localhost, not after deploy. Tried a bazillion cors settings.

saostad commented 4 years ago

Can you specify what exact Cors settings on the server did you set up? I'm having the same exact issue for over a month. Works in localhost, not after deploy. Tried a bazillion cors settings.

what is your server?

chemicalkosek commented 4 years ago

Omg after struggling with this issue for more than a month, I finally found the solution. The cors settings where fine. But in my mutation when setting the cookie you have to specifically set in res.cookie:

{domain: '.yourdomain.com'} Only then will the cookie persist during refresh when backend is at backend.yourdomain.com and frontend at frontend.yourdomain.com. It was working on localhost because it's the same domain: localhost

MattXtrem commented 4 years ago

Hi @chemicalkosek I got the same issue since one week and I don't understand.. Can you tell me what CORS settings you're using ? I tried to add the domain in the cookie but still does not work.. :/

This is how i set up cookie context.response.cookie("token", token, { domain: ".whos.now.sh", secure: true, httpOnly: true, maxAge: 1000 * 60 * 60 * 24 * 365 // 1 year });

that's the CORS configuration cors: { credentials: true, origin: process.env.fronturl, }

chemicalkosek commented 4 years ago

Not sure, if you're using secure: true then you need to have ssl ('https'). Do you have that? Also I'm not sure if you can set cookies on now.sh Source: https://twitter.com/wesbos/status/1062776904598650880

MattXtrem commented 4 years ago

Yes I use https. Well, here is the problem probably ! I will try tomorrow ! A big thank you for your answer, I let you know

nadzic commented 4 years ago

Here's my modification of the official nextjs example, which passing the cookie from nextjs ctx to fetch on ssr.

import React from "react"
import Head from "next/head"
import { ApolloProvider } from "@apollo/react-hooks"
import { ApolloClient } from "apollo-client"
import { InMemoryCache } from "apollo-cache-inmemory"
import { HttpLink } from "apollo-link-http"
import fetch from "isomorphic-unfetch"

let apolloClient = null

/**
 * Creates and provides the apolloContext
 * to a next.js PageTree. Use it by wrapping
 * your PageComponent via HOC pattern.
 * @param {Function|Class} PageComponent
 * @param {Object} [config]
 * @param {Boolean} [config.ssr=true]
 */
export function withApollo(PageComponent, { ssr = true } = {}) {
    const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
        const client = apolloClient || initApolloClient(apolloState)

        return (
            <ApolloProvider client={client} >
                <PageComponent {...pageProps} />
            </ApolloProvider>
        )
    }

    // Set the correct displayName in development
    if (process.env.NODE_ENV !== "production") {
        const displayName =
            PageComponent.displayName || PageComponent.name || "Component"

        if (displayName === "App") {
            console.warn("This withApollo HOC only works with PageComponents.")
        }

        WithApollo.displayName = `withApollo(${displayName})`
    }

    if (ssr || PageComponent.getInitialProps) {
        WithApollo.getInitialProps = async ctx => {
            const { AppTree } = ctx

            // Initialize ApolloClient, add it to the ctx object so
            // we can use it in `PageComponent.getInitialProp`.
            const apolloClient = (ctx.apolloClient = initApolloClient({}, ctx.req.headers.cookie))

            // Run wrapped getInitialProps methods
            let pageProps = {}
            if (PageComponent.getInitialProps) {
                pageProps = await PageComponent.getInitialProps(ctx)
            }

            // Only on the server:
            if (typeof window === "undefined") {
                // When redirecting, the response is finished.
                // No point in continuing to render
                if (ctx.res && ctx.res.finished) {
                    return pageProps
                }

                // Only if ssr is enabled
                if (ssr) {
                    try {
                        // Run all GraphQL queries
                        const { getDataFromTree } = await import("@apollo/react-ssr")
                        await getDataFromTree(
                            <AppTree
                                pageProps={{
                                    ...pageProps,
                                    apolloClient
                                }}
                            />
                        )
                    } catch (error) {
                        // Prevent Apollo Client GraphQL errors from crashing SSR.
                        // Handle them in components via the data.error prop:
                        // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
                        console.error("Error while running `getDataFromTree`", error)
                    }

                    // getDataFromTree does not call componentWillUnmount
                    // head side effect therefore need to be cleared manually
                    Head.rewind()
                }
            }

            // Extract query data from the Apollo store
            // @ts-ignore
            const apolloState = apolloClient.cache.extract()

            return {
                ...pageProps,
                apolloState
            }
        }
    }

    return WithApollo
}

/**
 * Always creates a new apollo client on the server
 * Creates or reuses apollo client in the browser.
 * @param  {Object} initialState
 */
function initApolloClient(initialState = {}, cookie = "") {
    // Make sure to create a new client for every server-side request so that data
    // isn"t shared between connections (which would be bad)

    if (typeof window === "undefined") {
        return createApolloClient(initialState, cookie)
    }

    // Reuse client on the client-side
    if (!apolloClient) {
        // @ts-ignore
        apolloClient = createApolloClient(initialState)
    }

    return apolloClient
}

/**
 * Creates and configures the ApolloClient
 * @param  {Object} [initialState={}]
 */
function createApolloClient(initialState = {}, cookie) {
    const enchancedFetch = (url, init) => {
        return fetch(url, {
            ...init,
            headers: {
                ...init.headers,
                "Cookie": cookie
            }
        }).then(response => response)
    }

    // Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient
    return new ApolloClient({
        ssrMode: typeof window === "undefined", // Disables forceFetch on the server (so queries are only run once)
        link: new HttpLink({
            uri: "http://localhost:3000/api", // Server URL (must be absolute)
            credentials: "same-origin", // Additional fetch() options like `credentials` or `headers`
            fetch: enchancedFetch
        }),
        cache: new InMemoryCache().restore(initialState)
    })
}

I see you are getting cookie via ctx.req.headers.cookie, can you also please show some example how in component you can set cookie to be accessible like this? I've just tried to use kind of setting cookie, but then the cookie is just available after refresh / swith between sites. So, therefore immeditaly after I do log-in it does not set header with cookie, but after you refresh or switch site (when apollo is reinitalized).

I used for example:

const { myCookie } = cookies(ctx); (trying to get cookie), but as I said it was not available (also not in setContext).

Is there any solution?

chemicalkosek commented 4 years ago

@nadzic There are no headers on the client side, so you can't access them. You need to somehow use document.cookie or maybe some other package like universal-cookie or react-cookie

derozan10 commented 4 years ago

The newest nextjs example works with a seperate apolloClient.js file. Modified it like so (based on mzygmunt's code).

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import fetch from 'isomorphic-unfetch';
import { endpoint } from '../config';

export default function createApolloClient(initialState, ctx) {
  // The `ctx` (NextPageContext) will only be present on the server.
  // use it to extract auth headers (ctx.req) or similar.

  const enchancedFetch = (url, init) =>
    fetch(url, {
      ...init,
      headers: {
        ...init.headers,
        Cookie: ctx.req.headers.cookie,
      },
    }).then(response => response);

  return new ApolloClient({
    ssrMode: Boolean(ctx),
    link: new HttpLink({
      uri: endpoint, // Server URL (must be absolute)
      credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers`
      fetch: ctx ? enchancedFetch : fetch,
    }),
    cache: new InMemoryCache().restore(initialState),
  });
}
mjurincic commented 4 years ago

@derozan10 Can you point which nextjs example are you referring to.

derozan10 commented 4 years ago

@derozan10 Can you point which nextjs example are you referring to.

this one

mjurincic commented 4 years ago

@derozan10 Can you point which nextjs example are you referring to.

this one

Thanks!

Does anybody know if this already works out of the box with https://github.com/lfades/next-with-apollo, i have forwarded cookie headers explicitly but still seems SSR doesn't behave correctly

import { ApolloProvider } from '@apollo/react-hooks';
import withApollo from 'next-with-apollo';
import ApolloClient, { InMemoryCache } from 'apollo-boost';

export default withApollo(
  ({ initialState, headers }: any) => {
    return new ApolloClient({
      credentials: 'include',
      uri: 'https://www.my-api.com',
      headers: {
        cookie: headers?.cookie,
      },
      cache: new InMemoryCache().restore(initialState || {}),
    });
  },
  {
    render: ({ Page, props }) => {
      const { apollo } = props;
      return (
        <ApolloProvider client={apollo}>
          <Page {...props} />
        </ApolloProvider>
      );
    },
  },
);
chemicalkosek commented 4 years ago

@mjurincic You'll need to pass getDataFromTree in next-with-apollo, read the docs:

// import { getDataFromTree } from '@apollo/react-ssr';
// You can also override the configs for withApollo here, so if you want
// this page to have SSR (and to be a lambda) for SEO purposes and remove
// the loading state, uncomment the import at the beginning and this:
//
// export default withApollo(Index, { getDataFromTree });
mjurincic commented 4 years ago

Thanks,

I didn't mention I'm already passing getDataFromTree it may be that err is somewhere else, I'll update ticket if I figure it out.

sushilbansal commented 4 years ago

Hi @mjurincic: did you figure out the issue?

EliasTouil commented 4 years ago

The newest nextjs example works with a seperate apolloClient.js file. Modified it like so (based on mzygmunt's code).

@derozan10 's and @mzygmunt code works with NextJs + Graphql Yoga + Prisma https://github.com/apollographql/apollo-client/issues/5089#issuecomment-598671071

I also changed my CORS options on the backend

...
const cors = require("cors");

const server = createServer();

var corsOptions = {
  origin: process.env.FRONTEND_URL,
  credentials: true
};
server.express.use(cors(corsOptions));
...

I also updated the dependencies until I could reach a 'no yarn warning' state

"dependencies": {
    "@apollo/react-hooks": "^3.1.5",
    "@apollo/react-ssr": "^3.1.5",
    "@babel/core": "^7.1.2",
    "@types/react": "^16.8.0",
    "apollo-cache-inmemory": "^1.6.6",
    "apollo-client": "^2.6.9",
    "apollo-link-http": "^1.5.17",
    "apollo-utilities": "^1.3.2",
    "graphql": "14.3.1",
    "graphql-tag": "^2.10.3",
    "isomorphic-unfetch": "^3.0.0",
    "next": "latest",
    "prop-types": "^15.6.2",
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  },
  "devDependencies": {
    "babel-plugin-graphql-tag": "^2.5.0"
  }
ODelibalta commented 4 years ago

I've tried the withapollo examples here with a separate createApollo and without. Either way I get headers undefined on Router.push which is what I use after a successful login. I think it has something to do with being server side or not because if I refresh the home page, everything is fine. I am new to this and I'd greatly appreciate your help. Tried credentials: include as well.

login.js section

  const [loginUser, { loading }] = useMutation(LOGIN_USER, {
    errorPolicy: 'all',
    onCompleted: (res) => {
      if (res.login) {
        Router.push('/');
      } else {
        setState((prevState) => ({
          ...prevState,
          loginError: true,
        }));
      }
    },
  });
.
.
.
export default withApollo(Login);

Screen Shot 2020-07-04 at 12 57 56 PM

dmitry commented 4 years ago

@ODelibalta what does the ctx shows?

ODelibalta commented 4 years ago

I changed the line

  if (ssr || PageComponent.getInitialProps) {

to

  if (typeof window === "undefined" || PageComponent.getInitialProps) {

and it works. Here is the full withApollo.js file for reference

import React from "react";
import Head from "next/head";
import { ApolloProvider } from "@apollo/react-hooks";
import createApolloClient from "./create";

let apolloClient = null;

/**
 * Creates and provides the apolloContext
 * to a next.js PageTree. Use it by wrapping
 * your PageComponent via HOC pattern.
 * @param {Function|Class} PageComponent
 * @param {Object} [config]
 * @param {Boolean} [config.ssr=true]
 */
export function withApollo(PageComponent, { ssr = true } = {}) {
  const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
    const client = apolloClient || initApolloClient(apolloState);

    return (
      <ApolloProvider client={client}>
        <PageComponent {...pageProps} />
      </ApolloProvider>
    );
  };

  // Set the correct displayName in development
  if (process.env.NODE_ENV !== "production") {
    const displayName =
      PageComponent.displayName || PageComponent.name || "Component";

    if (displayName === "App") {
      console.warn("This withApollo HOC only works with PageComponents.");
    }

    WithApollo.displayName = `withApollo(${displayName})`;
  }

  if (typeof window === "undefined" || PageComponent.getInitialProps) {
    WithApollo.getInitialProps = async (ctx) => {
      const { AppTree } = ctx;

      // Initialize ApolloClient, add it to the ctx object so
      // we can use it in `PageComponent.getInitialProp`.
      const apolloClient = await (ctx.apolloClient = initApolloClient(
        {},
        ctx.req.headers.cookie
      ));

      // Run wrapped getInitialProps methods
      let pageProps = {};
      if (PageComponent.getInitialProps) {
        pageProps = await PageComponent.getInitialProps(ctx);
      }

      // Only on the server:
      if (typeof window === "undefined") {
        // When redirecting, the response is finished.
        // No point in continuing to render
        if (ctx.res && ctx.res.finished) {
          return pageProps;
        }

        // Only if ssr is enabled
        if (ssr) {
          try {
            // Run all GraphQL queries
            const { getDataFromTree } = await import("@apollo/react-ssr");
            await getDataFromTree(
              <AppTree
                pageProps={{
                  ...pageProps,
                  apolloClient,
                }}
              />
            );
          } catch (error) {
            // Prevent Apollo Client GraphQL errors from crashing SSR.
            // Handle them in components via the data.error prop:
            // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
            console.error("Error while running `getDataFromTree`", error);
          }

          // getDataFromTree does not call componentWillUnmount
          // head side effect therefore need to be cleared manually
          Head.rewind();
        }
      }

      // Extract query data from the Apollo store
      // @ts-ignore
      const apolloState = apolloClient.cache.extract();

      return {
        ...pageProps,
        apolloState,
      };
    };
  }

  return WithApollo;
}

/**
 * Always creates a new apollo client on the server
 * Creates or reuses apollo client in the browser.
 * @param  {Object} initialState
 */
function initApolloClient(initialState = {}, cookie = "") {
    // Make sure to create a new client for every server-side request so that data
    // isn"t shared between connections (which would be bad)

    if (typeof window === "undefined") {
        return createApolloClient(initialState, cookie)
    }

    // Reuse client on the client-side
    if (!apolloClient) {
        // @ts-ignore
        apolloClient = createApolloClient(initialState)
    }

    return apolloClient
}

and the create.js file

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { createHttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import Cookies from 'js-cookie';
import { serverUrl } from '../config';

export default function createApolloClient(initialState, ctx) {
  // The `ctx` (NextPageContext) will only be present on the server.
  // use it to extract auth headers (ctx.req) or similar.

  const authLink = setContext((_, { headers  }) => {
    // get the authentication token from local storage if it exists
    const token = Cookies.get("XSRF-TOKEN");
    // return the headers to the context so httpLink can read them
    return {
      headers: {
        ...headers,
        "Access-Control-Allow-Credentials": true,
        ...(token ? { "X-CSRF-TOKEN": `${token}` } : {}),
        ...(token ? { "authorization": `Bearer ${token}` } : {}),
      },
    };
  });

  const httpLink = createHttpLink({
    uri: serverUrl,
    credentials: 'same-origin',
  });

  return new ApolloClient({
    ssrMode: Boolean(ctx),
    link: authLink.concat(httpLink),
    connectToDevTools: true,
    cache: new InMemoryCache().restore(initialState),
  });
}
lionelyoung commented 4 years ago

Here's my modification of the official nextjs example, which passing the cookie from nextjs ctx to fetch on ssr.

...
export function withApollo(PageComponent, { ssr = true } = {}) {
    const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
        const client = apolloClient || initApolloClient(apolloState)

        return (
            <ApolloProvider client={client} >
                <PageComponent {...pageProps} />
            </ApolloProvider>
        )
    }

    // Set the correct displayName in development
    if (process.env.NODE_ENV !== "production") {
        const displayName =
            PageComponent.displayName || PageComponent.name || "Component"

        if (displayName === "App") {
            console.warn("This withApollo HOC only works with PageComponents.")
        }

        WithApollo.displayName = `withApollo(${displayName})`
    }

    if (ssr || PageComponent.getInitialProps) {
        WithApollo.getInitialProps = async ctx => {
            const { AppTree } = ctx
.....

@mzygmunt How do you combine this with getServersideProps? I need to pull the context, and I get an error Error: You can not use getInitialProps with getServerSideProps. Please remove getInitialProps.


export async function getServerSideProps(context) {
  const myVars = {
    orderId: context["params"]["order"]
  }

  return {
    props: {
      myVars: myVars,
    },
  }
}
codingcn commented 4 years ago

I successfully solved this problem in nextjs, it is actually very simple, just need to pass ctx in the getServerSideProps of the page component.

pages/_app.js

import {Provider} from 'react-redux'
import {ApolloProvider} from '@apollo/react-hooks'
import {useApollo} from '../libs/apolloClient'
import '../asserts/global.css'
import store from '../redux/store'

const App = ({Component, pageProps}) => {
    const apolloClient = useApollo(pageProps.initialApolloState)
    return (
        <Provider store={store}>
            <ApolloProvider client={apolloClient}>
                <Component {...pageProps} />
            </ApolloProvider>
        </Provider>
    )

}

export default App

../libs/apolloClient.js

import {useMemo} from 'react'
import {getCookie} from "./cookie";
import {ApolloClient, createHttpLink, InMemoryCache} from '@apollo/client';
import {setContext} from '@apollo/client/link/context';

let apolloClient

function createApolloClient(ctx) {
    let headers = {}
    let accessToken=""
    if (ctx) {
        accessToken = getCookie('accessToken',ctx.req);
        if (accessToken) {
            headers = {
                Authorization: accessToken,
            }
        }
    }

    const httpLink = createHttpLink({
        uri: 'http://0.0.0.0:9900/query',
    });
    const authLink = setContext((req, {headers}) => {
        // console.log(req, headers)
        // get the authentication token from local storage if it exists
        // return the headers to the context so httpLink can read them
        return {
            headers: {
                ...headers,
                authorization: accessToken ? `Bearer ${accessToken}` : "",
            }
        }
    });
    return new ApolloClient({
        ssrMode: typeof window === 'undefined',
        link: authLink.concat(httpLink),
        cache: new InMemoryCache(),
        credentials: 'include',
        headers: headers
        // defaultOptions: defaultOptions
    })
}

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

    // If your page has Next.js data fetching methods that use Apollo Client, the initial state
    // get hydrated here
    if (initialState) {
        _apolloClient.cache.restore(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, ctx) {
    const store = useMemo(() => initializeApollo(initialState, ctx), [initialState, ctx])
    return store
}

pages/home.js

import {initializeApollo} from '../libs/apolloClient'
import {articleQuery, recommendRelatedArticlesQuery} from '../gql/article.query'
import {useRouter} from 'next/router';
import {useApolloClient} from "@apollo/react-hooks";

const Home = ({initialApolloState}) => {

    const router = useRouter();
    const apolloClient = useApolloClient()

    const {recommendRelatedArticles} = apolloClient.restore(initialApolloState).readQuery(
        {
            query: recommendRelatedArticlesQuery,
            variables: {
                articleId: "1nGtGEH6En",
                page: 1,
                pageSize: 5,
            }
        }, false
    )
    const {article} = apolloClient.restore(initialApolloState).readQuery(
        {
            query: articleQuery,
            variables: {
                sid: "1nGtGEH6En",
            }
        }, false
    )
    console.log(recommendRelatedArticles,article)
    return (
        <>
        </>
    )
}

export async function getServerSideProps(context) {
    const apolloClient = initializeApollo(null, context)
    await apolloClient.query({
        query: articleQuery,
        variables: {
            sid: "1nGtGEH6En",
        }
    }).then(result => {

    }).catch(e => {
        console.log(e.message)
    });
    await apolloClient.query({
        query: recommendRelatedArticlesQuery,
        variables: {
            articleId: "1nGtGEH6En",
            page: 1,
            pageSize: 5,
        }
    }).then(result => {

    }).catch(e => {
        console.log(e.message)
    });
    return {
        props: {
            initialApolloState: apolloClient.cache.extract(true),
        },
    }
}

export default Home
shoaibsharif commented 4 years ago

@codingcn Your config almost worked for me. I just couldn't make it work for SSR but it definitely works with Client side for me on production. Here is a simple version I did: https://codesandbox.io/s/goofy-johnson-sp8kb?file=/pages/index.tsx One thing to note that when I don't get issue when I run on localhost. How did you get the cookie on server-side? It turns out I can't get the cookie on server side (getServersideProps context) at all, but on client side I get the cookie and everything works just fine.

platypusrex commented 3 years ago

@codingcn @shoaibsharif My implementation is also the exact same. Curious about one difference I noticed with useApollo hook:

export function useApollo(initialState, ctx) {
    const store = useMemo(() => initializeApollo(initialState, ctx), [initialState, ctx])
    return store
}

Pretty sure you can remove ctx param here as context is only passed when initializeApollo is call from getServerSideProps. The only relevant data here is hydrated server side (from the page component) and then passed to the hook via page props (in _app.tsx). No context necessary.

phattranky commented 3 years ago

Those changes are working at my side to send the cookie from NextJS Server Side Rendering to the Server (I'm using NestJS)

111

The full withApollo.tsx

import React from 'react';
import App from 'next/app';
import Head from 'next/head';
import { ApolloProvider } from '@apollo/react-hooks';
import createApolloClient from './apolloClient';
// On the client, we store the Apollo Client in the following variable.
// This prevents the client from reinitializing between page transitions.
let globalApolloClient = null;
/**
 * Installs the Apollo Client on NextPageContext
 * or NextAppContext. Useful if you want to use apolloClient
 * inside getStaticProps, getStaticPaths or getServerSideProps
 * @param {NextPageContext | NextAppContext} ctx
 */
export const initOnContext = (ctx) => {
  const inAppContext = Boolean(ctx.ctx);
  // We consider installing `withApollo({ ssr: true })` on global App level
  // as antipattern since it disables project wide Automatic Static Optimization.
  if (process.env.NODE_ENV === 'development') {
    if (inAppContext) {
      console.warn(
        'Warning: You have opted-out of Automatic Static Optimization due to `withApollo` in `pages/_app`.\n' +
          'Read more: https://err.sh/next.js/opt-out-auto-static-optimization\n'
      );
    }
  }
  // Initialize ApolloClient if not already done
  const apolloClient =
    ctx.apolloClient ||
    initApolloClient(ctx.apolloState || {}, inAppContext ? ctx.ctx : ctx);
  // We send the Apollo Client as a prop to the component to avoid calling initApollo() twice in the server.
  // Otherwise, the component would have to call initApollo() again but this
  // time without the context. Once that happens, the following code will make sure we send
  // the prop as `null` to the browser.
  apolloClient.toJSON = () => null;
  // Add apolloClient to NextPageContext & NextAppContext.
  // This allows us to consume the apolloClient inside our
  // custom `getInitialProps({ apolloClient })`.
  ctx.apolloClient = apolloClient;
  if (inAppContext) {
    ctx.ctx.apolloClient = apolloClient;
  }
  return ctx;
};

async function getHeaders(ctx) {
  // if (typeof window !== 'undefined') return null
  // if (typeof ctx.req === 'undefined') return null
  // const s = await auth0.getSession(ctx.req)
  // if (s && s.accessToken == null) return null
  // return {
  //   authorization: `Bearer ${s ? s.accessToken: ''}`
  // }
  console.log('ooo', ctx?.req?.cookies)
  if (ctx?.req?.cookies) {
    const cookieItems = []
    for (let key of Object.keys(ctx?.req?.cookies)) {
      cookieItems.push(`${key}=${ctx.req.cookies[key]}`)
    }
    return {
      cookie: cookieItems.join('; ')
    }
  }

  return {
  }
}

/**
 * Always creates a new apollo client on the server
 * Creates or reuses apollo client in the browser.
 * @param  {NormalizedCacheObject} initialState
 * @param  {NextPageContext} ctx
 */
const initApolloClient = (initialState, headers) => {
  // Make sure to create a new client for every server-side request so that data
  // isn't shared between connections (which would be bad)
  if (typeof window === 'undefined') {
    return createApolloClient(initialState, headers);
  }
  // Reuse client on the client-side
  if (!globalApolloClient) {
    globalApolloClient = createApolloClient(initialState, headers);
  }
  return globalApolloClient;
};
/**
 * Creates a withApollo HOC
 * that provides the apolloContext
 * to a next.js Page or AppTree.
 * @param  {Object} withApolloOptions
 * @param  {Boolean} [withApolloOptions.ssr=false]
 * @returns {(PageComponent: ReactNode) => ReactNode}
 */
export const withApollo = ({ ssr = true } = {}) => (PageComponent): any => {
  const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
    let client;
    if (apolloClient) {
      // Happens on: getDataFromTree & next.js ssr
      client = apolloClient;
    } else {
      // Happens on: next.js csr
      // client = initApolloClient(apolloState, undefined);
      client = initApolloClient(apolloState, {});
    }
    return (
      <ApolloProvider client={client}>
        <PageComponent {...pageProps} />
      </ApolloProvider>
    );
  };
  // Set the correct displayName in development
  if (process.env.NODE_ENV !== 'production') {
    const displayName =
      PageComponent.displayName || PageComponent.name || 'Component';
    WithApollo.displayName = `withApollo(${displayName})`;
  }
  if (ssr || PageComponent.getInitialProps) {
    WithApollo.getInitialProps = async (ctx) => {
      const { AppTree } = ctx
      // Initialize ApolloClient, add it to the ctx object so
      // we can use it in `PageComponent.getInitialProp`.
      const apolloClient = (ctx.apolloClient = initApolloClient(null, await getHeaders(ctx)))
      // Run wrapped getInitialProps methods
      let pageProps = {}
      if (PageComponent.getInitialProps) {
        pageProps = await PageComponent.getInitialProps(ctx)
      }
      // Only on the server:
      if (typeof window === 'undefined') {
        // When redirecting, the response is finished.
        // No point in continuing to render
        if (ctx.res && ctx.res.finished) {
          return pageProps
        }
        // Only if ssr is enabled
        if (ssr) {
          try {
            // Run all GraphQL queries
            const { getDataFromTree } = await import('@apollo/react-ssr')
            await getDataFromTree(
              <AppTree
                pageProps={{
                  ...pageProps,
                  apolloClient
                }}
              />
            )
          } catch (error) {
            // Prevent Apollo Client GraphQL errors from crashing SSR.
            // Handle them in components via the data.error prop:
            // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
            console.error('Error while running `getDataFromTree`', error)
          }
          // getDataFromTree does not call componentWillUnmount
          // head side effect therefore need to be cleared manually
          Head.rewind()
        }
      }
      // Extract query data from the Apollo store
      const apolloState = apolloClient.cache.extract()
      return {
        ...pageProps,
        apolloState
      }
    }
  }
  return WithApollo;
};

The apolloClient.ts

import fetch from 'isomorphic-unfetch'
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { HttpLink } from 'apollo-link-http'
import { ApolloLink, Observable } from 'apollo-link';
import { onError } from 'apollo-link-error'
import { useMutation } from '@apollo/react-hooks';
// import { WebSocketLink } from 'apollo-link-ws'
// import { SubscriptionClient } from 'subscriptions-transport-ws'

let accessToken = null
const requestAccessToken = async () => {
  if (accessToken) return
  const res = await fetch(`${process.env.APP_HOST}/api/session`)
  if (res.ok) {
    const json = await res.json()
    accessToken = json.accessToken
  } else {
    accessToken = 'public'
  }
}

// remove cached token on 401 from the server
// const resetTokenLink = onError(({ networkError }) => {
//   if (networkError && networkError.name === 'ServerError' && networkError.statusCode === 401) {
//     accessToken = null
//   }
// })

const request = async (operation: any) => {
  // await renewalToken();

  // const token = localStorage.getItem('user_token');
  operation.setContext({
    headers: {
      // 'custom-header': 'custom'
      // authorization: `Bearer ${token}`,
      // 'x-tag': xTagGenerator()
    }
  });
};

const requestLink = new ApolloLink((operation: any, forward: any) =>
  new Observable((observer: any) => {
    let handle: any;
    Promise.resolve(operation)
      .then(oper => request(oper))
      .then(() => {
        handle = forward(operation).subscribe({
          next: observer.next.bind(observer),
          error: observer.error.bind(observer),
          complete: observer.complete.bind(observer),
        });
      })
      .catch(observer.error.bind(observer));

    return () => {
      if (handle) handle.unsubscribe();
    };
  })
);

const createHttpLink = (headers) => {
  const httpLink = new HttpLink({
    uri: `${process.env.API_URL}/graphql`,
    credentials: 'include',
    headers, // auth token is fetched on the server side
    fetch,
  })
  return httpLink;
}

// const createWSLink = () => {
//   return new WebSocketLink(
//     new SubscriptionClient('wss://ready-panda-91.hasura.app/v1/graphql', {
//       lazy: true,
//       reconnect: true,
//       connectionParams: async () => {
//         // happens on the client
//         await requestAccessToken()
//         return {
//           headers: {
//             authorization: accessToken ? `Bearer ${accessToken}` : '',
//           },
//         }
//       },
//     })
//   )
// }

export default function createApolloClient(initialState, headers) {
  const ssrMode = typeof window === 'undefined'
  const httpLink = createHttpLink(headers)
  // let link
  // if (ssrMode) {
  //   link = createHttpLink(headers) // executed on server
  // } else {
  //   link = createWSLink() // executed on client
  // }
  return new ApolloClient({
    ssrMode,
    link: ApolloLink.from([
      requestLink,
      httpLink,
    ]),
    cache: new InMemoryCache().restore(initialState),
  })
}
Tokenyet commented 3 years ago

From @codingcn 's code, there is an issue when you use full SSR, that used getInitialProps in _app.ts. This would happen when user gets the TOKEN at first time and be navigated to another page.

Here is the patch for the code:

    const authLink = setContext((req, {headers}) => {
        const clientSideToken = getCookie('accessToken',ctx.req); // Use your regular method to get token from client side
        if(!accessToken) accessToken  = clientSideToken;
        return {
            headers: {
                ...headers,
                authorization: accessToken ? `Bearer ${accessToken}` : "",
            }
        }
    });
zobayerhossainmamun commented 3 years ago

Please add cookies from your route and get cookie from your init route and pass it into props. I hope it will work. Or show your auth.js file code.

SysSU commented 2 years ago

I'm pretty much using the same setup used here https://github.com/vercel/next.js/tree/canary/examples/with-apollo except I get an auth token and set it up in the headers when creating the ApolloClient as shown below. I'm running into the same issue others have expressed where the authCookie below is coming back as null until I refresh the page. Has anyone figured out how to solve this? Right now the workaround I have is that if I get an auth error I reload the page...however this is not ideal as I can end up in endless reloads.

For auth and how the cookie is set basically what happens is I'm using Auth0 and Auth0 redirects to a signed-in.js page that sets the cookie and then redirects using Next.js Router to the index.js page.

import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
import Cookie from 'js-cookie';
import { endpoint } from '../config';
// ....
function createApolloClient() {
  const isBrowser = typeof window !== 'undefined';
  const authCookie = (isBrowser) ? Cookie.get('idToken') : null;

  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(),
  });
}
bignimbus commented 1 year ago

Hi all, I'm doing a bit of housekeeping. Since the most recent activity on this issue is more than a year old and there's no clear action to be taken by the maintainers, I'll close this for now. Happy to re-open if anyone feels strongly that there's a code or documentation update that should be made. Also feel free to post to our community forum. Thanks so much!

wwwhatley commented 1 year ago

This should not be closed because the solutions provided are not adequate.

bignimbus commented 1 year ago

Hi @wwwhatley 👋🏻 can you share more details about your use case and expected vs. actual behavior?

github-actions[bot] commented 1 year ago

We're closing this issue now but feel free to ping the maintainers or open a new issue if you still need support. Thank you!