apollo-client-nextjs icon indicating copy to clipboard operation
apollo-client-nextjs copied to clipboard

useSuspenseQuery not sending cookies on Server Side query

Open ODreelist opened this issue 1 year ago • 19 comments

Hi there,

I may be missing something simple here but I have an apollo-provider that is pretty standard, however we use supabase auth and the session is set in cookies.

Obviously the cookie is present when the user refreshes the query page, but its not until the client re-initiates the query (which I don't think would happen if this issue was solved) that the server sees the cookies passed. The httpLink and client set up is as follows:

` const isServer = typeof window === "undefined"; const httpLink = new HttpLink({ uri: GRAPHQL_URI, credentials: "include" });

return new NextSSRApolloClient({ cache: apolloCache, link: isServer ? ApolloLink.from([new SSRMultipartLink({ stripDefer: true }), httpLink]) : httpLink, }); }; `

I know the cookie is available because I can set it as an authorization header which does get fired in the query on the server side but I'd rather not have to "hack" it that way, Is there a way to ensure that even when query is processed on the server side that it includes the cookies?

Thanks in advance.

ODreelist avatar Aug 13 '23 22:08 ODreelist

The problem is that the server does per default not have any connection between cookies of the incoming request (browser to Next server) and outgoing requests (server to GraphQL server).

You will have to extract the cookie from the incoming request and then manually pass it on.

Unfortunately, in Next.js, Client Components cannot access the cookies of the incoming request - only server components can. So you need to:

  • in a server component call cookies()
  • extract the cookie you need
  • pass it as props to a Client Component (your ApolloProviderWrapper)
  • if isServer, manually add that cookie to your HttpLink.

plausible-phry avatar Aug 15 '23 01:08 plausible-phry

I appreciate the thoughtful response, that's essentially my current implementation, except I manually add the token from the cookie to the HttpLink via headers.authorization. Is that what you mean? Or is there a way to actually manually add the cookie to the HttpLink that I'm not aware of.

ODreelist avatar Aug 17 '23 01:08 ODreelist

Is that what you mean?

I'd pass it into the HttpLink constructor via headers.authorization option, so probably what you are doing now :)

plausible-phry avatar Aug 17 '23 11:08 plausible-phry

pass it as props to a Client Component (your ApolloProviderWrapper)

Forgive me if I'm missing something obvious, but if you have an HTTP-only cookie that holds a sensitive credential, and then pass it to a client component, don't you risk exposing it to client-side code and introducing an XSS vulnerability?

Is the thinking here that unless you're actually rendering the cookie value it shouldn't show up in any part of the browser payload? Edit: looks like props passed to client components can show up in the browser payload, even if they're not used directly:

self.__next_f.push([1,"c:I{\"id\":\"(app-client)/./src/app/org/Name.tsx\",\"chunks\":[\"app/org/page:static/chunks/app/org/page.js\"],\"name\":\"\",\"async\":false}\nb:[[\"$\",\"h1\",null,{\"children\":[\"Hello, \",\"Michael Bluth\",\"!\"]}],[\"$\",\"$Lc\",null,{\"secret\":\"MY_SUPER_SECRET_VALUE\"}]]\n"])

rval avatar Oct 07 '23 17:10 rval

@rval I'd love to give you a better answer, but this seems like an architectural oversight on the side of Next.js - we won't be able to give you any better advice until they come up with a better way of doing this.

Maybe open an issue over there, and if they come up with something better, please report back here? I'd love to hear about that :)

phryneas avatar Oct 09 '23 09:10 phryneas

pass it as props to a Client Component (your ApolloProviderWrapper)

Forgive me if I'm missing something obvious, but if you have an HTTP-only cookie that holds a sensitive credential, and then pass it to a client component, don't you risk exposing it to client-side code and introducing an XSS vulnerability?

Is the thinking here that unless you're actually rendering the cookie value it shouldn't show up in any part of the browser payload? Edit: looks like props passed to client components can show up in the browser payload, even if they're not used directly:

self.__next_f.push([1,"c:I{\"id\":\"(app-client)/./src/app/org/Name.tsx\",\"chunks\":[\"app/org/page:static/chunks/app/org/page.js\"],\"name\":\"\",\"async\":false}\nb:[[\"$\",\"h1\",null,{\"children\":[\"Hello, \",\"Michael Bluth\",\"!\"]}],[\"$\",\"$Lc\",null,{\"secret\":\"MY_SUPER_SECRET_VALUE\"}]]\n"])

Here's what I did, make a new env variable name it anything you'd like but don't start it with "NEXT_PUBLIC_" (otherwise that will be bundled in browser) for instance I chose : ENCRYPTION_KEY="12345678"

Now before passing your cookies to a child client component encrypt it using that key in your layout file, in your client component (ApolloWrapper) decrypt it only if typeof window === "undefined" and attach it.

Since no one will have access to your encryption key all your cookies will be encrypted and decrypted safely on the server side, but remember to add some random data into your object before encryption otherwise it might still be easy to crack.

It's not the best we could do but it's probably the best we can do, if NextJS team added some global request object for the server side, we didn't have to deal with all these non-sense.

Stevemoretz avatar Oct 09 '23 18:10 Stevemoretz

That's incredibly hacky, but also a great solution - well done!

phryneas avatar Oct 10 '23 08:10 phryneas

We have encountered the same issue and it is so frustrating. Basically it blocks SSR support for hooks (unless we use a really smart but still hacky solution above). I have created a discussion in the Next.js repo, hopefully it will gain some attention.

iamkd avatar Oct 12 '23 12:10 iamkd

@Stevemoretz I went ahead and published your approach as a npm package that should make this a lot easier to utilize: https://www.npmjs.com/package/ssr-only-secrets

phryneas avatar Jan 04 '24 16:01 phryneas

import { setContext } from '@apollo/client/link/context';

const forwardCookieLink = setContext(async () => {
  return import('next/headers').then(({ cookies }) => {
    return {
      headers: {
        cookie: cookies()
          .getAll()
          .map(({ name, value }) => `${name}=${value}`)
          .join(';'),
      },
    };
  });
});

return new NextSSRApolloClient({
    // use the `NextSSRInMemoryCache`, not the normal `InMemoryCache`
    cache: new NextSSRInMemoryCache(),
    link:
      typeof window === "undefined"
        ? ApolloLink.from([
            // in a SSR environment, if you use multipart features like
            // @defer, you need to decide how to handle these.
            // This strips all interfaces with a `@defer` directive from your queries.
            new SSRMultipartLink({
              stripDefer: true,
            }),
            forwardCookieLink,
            httpLink,
          ])
        : httpLink,
  });

esavitskiy avatar Feb 19 '24 13:02 esavitskiy

@esavitskiy You cannot use cookies() outside of React Server Components, and NextSSRInMemoryCache is explicitly targeting Client Components (and this whole thread is about SSR of Client Components), so this seems like it wouldn't do what you expect.

phryneas avatar Feb 19 '24 13:02 phryneas

@esavitskiy You cannot use cookies() outside of React Server Components, and NextSSRInMemoryCache is explicitly targeting Client Components (and this whole thread is about SSR of Client Components), so this seems like it wouldn't do what you expect.

just try

esavitskiy avatar Feb 19 '24 14:02 esavitskiy

image

It seems that this is working in some way, but it is clearly not documented and might break with every update.

phryneas avatar Feb 19 '24 15:02 phryneas

I just verified with the Next.js support forum. This is not a stable feature of Next.js. Please don't do this.

image

phryneas avatar Feb 19 '24 16:02 phryneas

This is not a stable feature of Next.js. Please don't do this.

I agree, but it would be nice =)

esavitskiy avatar Feb 19 '24 16:02 esavitskiy

Is there any update on this?

Froncz avatar May 13 '24 12:05 Froncz

@Froncz I believe this has been answered, so what is your question after reading all the comments?

phryneas avatar May 13 '24 12:05 phryneas

import { setContext } from '@apollo/client/link/context';

const forwardCookieLink = setContext(async () => {
  return import('next/headers').then(({ cookies }) => {
    return {
      headers: {
        cookie: cookies()
          .getAll()
          .map(({ name, value }) => `${name}=${value}`)
          .join(';'),
      },
    };
  });
});

return new NextSSRApolloClient({
    // use the `NextSSRInMemoryCache`, not the normal `InMemoryCache`
    cache: new NextSSRInMemoryCache(),
    link:
      typeof window === "undefined"
        ? ApolloLink.from([
            // in a SSR environment, if you use multipart features like
            // @defer, you need to decide how to handle these.
            // This strips all interfaces with a `@defer` directive from your queries.
            new SSRMultipartLink({
              stripDefer: true,
            }),
            forwardCookieLink,
            httpLink,
          ])
        : httpLink,
  });

I spent a working day of hours trying to solve this issue, and your code worked perfectly, so thank you!

Joshibbotson avatar May 26 '24 17:05 Joshibbotson

import { setContext } from '@apollo/client/link/context';

const forwardCookieLink = setContext(async () => {
  return import('next/headers').then(({ cookies }) => {
    return {
      headers: {
        cookie: cookies()
          .getAll()
          .map(({ name, value }) => `${name}=${value}`)
          .join(';'),
      },
    };
  });
});

return new NextSSRApolloClient({
    // use the `NextSSRInMemoryCache`, not the normal `InMemoryCache`
    cache: new NextSSRInMemoryCache(),
    link:
      typeof window === "undefined"
        ? ApolloLink.from([
            // in a SSR environment, if you use multipart features like
            // @defer, you need to decide how to handle these.
            // This strips all interfaces with a `@defer` directive from your queries.
            new SSRMultipartLink({
              stripDefer: true,
            }),
            forwardCookieLink,
            httpLink,
          ])
        : httpLink,
  });

I spent a working day of hours trying to solve this issue, and your code worked perfectly, so thank you!

when i tried this, nextjs failed the build on this error: https://nextjs.org/docs/messages/dynamic-server-error

the package suggested works

marcusglowe avatar Jun 06 '24 02:06 marcusglowe