apollo-client-nextjs
apollo-client-nextjs copied to clipboard
useSuspenseQuery not sending cookies on Server Side query
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.
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 yourHttpLink
.
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.
Is that what you mean?
I'd pass it into the HttpLink
constructor via headers.authorization
option, so probably what you are doing now :)
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 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 :)
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.
That's incredibly hacky, but also a great solution - well done!
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.
@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
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 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.
@esavitskiy You cannot use
cookies()
outside of React Server Components, andNextSSRInMemoryCache
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
It seems that this is working in some way, but it is clearly not documented and might break with every update.
I just verified with the Next.js support forum. This is not a stable feature of Next.js. Please don't do this.
This is not a stable feature of Next.js. Please don't do this.
I agree, but it would be nice =)
Is there any update on this?
@Froncz I believe this has been answered, so what is your question after reading all the comments?
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!
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