Next.js Discord

Discord Forum

Receive cookies in Server Actions

Unanswered
Polar bear posted this in #help-forum
Open in Discord
Polar bearOP
Hello there, I have a difficult issue that I can't solve on my own.
I am trying to fetch the cookies inside a server action within pages/api/auth/client.ts. This is where I create the ApolloClient:

import {
  ApolloClient,
  ApolloLink,
  HttpLink,
  InMemoryCache,
} from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
import { getCookie } from 'cookies-next'
import dotenv from 'dotenv'

dotenv.config()

const getBackendUrl = (): string => {
  const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL
  if (!backendUrl) {
    throw new Error('NEXT_PUBLIC_BACKEND_URL is not set')
  }
  if (process.env.NODE_ENV === 'development') {
    return `http://${backendUrl}/graphql`
  } else if (process.env.NODE_ENV === 'production') {
    return `https://${backendUrl}/graphql`
  }
  throw new Error('NODE_ENV is not set or not recognized')
}

const httpLink = new HttpLink({
  uri: getBackendUrl(),
  credentials: 'include',
})

const authLink = setContext((_, { headers }) => {
  const token = getCookie('jwt')

  return {
    headers: {
      ...headers,
      'Authorization': token,
    },
  }
})

export const client = new ApolloClient({
  link: ApolloLink.from([authLink, httpLink]),
  cache: new InMemoryCache(),
})

The idea was to include the jwt which is stored inside the browser's cookies in the request header of each query with the client. Although this should work, I always receive undefined if I do console.log(token) inside the authLink method.
Is there any way to read the browser cookies inside the server actions, without a further implementation of passing an argument to my client.ts and create a client in every component that I need it in?
Thank you in advance.

28 Replies

Why not use the cookies() function from next? It can be used in server actions @Polar bear
Polar bearOP
Because then I receive this error:
I thought it was mandatory to place the server actions inside the api folder, which is inside the pages folder
Is it okay to put the server actions into a different folder? Because then I would just place them somewhere else.
The issue is that if I do that, I won't be able to send fetch requests to /api/*
and if I move the server actions into a different folder, and use use server at the top of the file, this is what I receive:
Oh this is pages dir? Sorry I'm an app dir guy. Wait for someone pages specific to go through this forum
nope @Polar bear you can get cookies using next/headers
https://nextjs.org/docs/app/api-reference/functions/cookies
oh, is it page router?
Polar bearOP
page router, yes
oh @Polar bear I think you should use a middleware
import { NextRequest, NextResponse } from 'next/server';

export function middleware(req: NextRequest) {
  const response = NextResponse.next();

  // Check if the cookie already exists
  if (!req.cookies.has('your-cookie-name')) {
    // Set the cookie if it doesn't exist
    response.cookies.set('your-cookie-name', 'your-cookie-value', {
      httpOnly: true, // Only accessible by the web server
      secure: process.env.NODE_ENV === 'production', // Send only over HTTPS in production
      sameSite: 'strict', // Prevents the browser from sending this cookie along with cross-site requests
      maxAge: 60 * 60 * 24 * 7, // 1 week
    });
  }

  return response;
}

export const config = {
  matcher: '/:path*', // Apply middleware to all routes
};

like this?
Hey @Polar bear - have you looked at the https://www.npmjs.com/package/cookies-next package? That has page router support too.
Polar bearOP
yea as said, the issue is that using cookies-next results in undefined inside the api folder for client.ts
Right now I'm actually switching off from apollo and re-structuring the logic of my client, moving to axios and so on
will keep you updated if that solved my problem.
Polar bearOP
I actually would love to not do that, as that creates too much overhead so I would like to ask:
How can I add the browser cookies to the request header of Apollo, if I am hosting frontend and backend in separater docker containers but inside the same network?
Since I can't use server actions, there must be another way...
My bad - I was getting confused between cookies-next and next-cookies. The only thing I notice that differs in your code the the cookies-next page router docs is they suggest using getCookie('jwt', { req, res }) - perhaps you need to inject the request and response in there and maybe that's why you're getting undefined?
But also, moving to axios will probably work too 👍
Polar bearOP
Sooo I solved the issue by understanding the logic behind nextJS without app router. Seems like server actions aren't a thing in the pages router. What happens is: Browser sends request to NextJS -> NextJS sends request to Backend URL via ApolloClient -> Backend responds.
In between there's the ApolloClient, which has nothing to do with the browser, meaning the header isn't the same.
I had to manually assign the cookie header to the ApolloClient so what I did was code a middleware that accepts the request with the cookie header and then it worked
// src/pages/api/auth/client.ts

import {
  ApolloClient,
  ApolloLink,
  HttpLink,
  InMemoryCache,
} from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
import dotenv from 'dotenv'

dotenv.config()

const getBackendUrl = (): string => {
  const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL
  if (!backendUrl) {
    throw new Error('NEXT_PUBLIC_BACKEND_URL is not set')
  }
  if (process.env.NODE_ENV === 'development') {
    return `http://${backendUrl}/graphql`
  } else if (process.env.NODE_ENV === 'production') {
    return `https://${backendUrl}/graphql`
  }
  throw new Error('NODE_ENV is not set or not recognized')
}

const httpLink = new HttpLink({
  uri: getBackendUrl(),
  credentials: 'include',
})

export const createClientWithAuth = (headers: Record<string, string> = {}) => {
  const authLink = setContext((_, { headers: existingHeaders }) => {
    return {
      headers: {
        ...existingHeaders,
        ...headers,
      },
    }
  })

  return new ApolloClient({
    link: ApolloLink.from([authLink, httpLink]),
    cache: new InMemoryCache(),
  })
}


example:
import { USER_INFO } from '@/gql'
import { NextApiRequest, NextApiResponse } from 'next'
import { createClientWithAuth } from './client'

const authHandler = async (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method === 'GET') {
    try {
      const headers = {
        cookie: req.headers.cookie || '',
      }
      const client = createClientWithAuth(headers)
      const { data } = await client.query({ query: USER_INFO })

      if (data && data.userInfo) {
        return res.status(200).json(data.userInfo)
      } else {
        return res.status(404).json({ error: "Couldn't authenticate user" })
      }
    } catch (error) {
      console.error(error)
      return res.status(500).json({ error: 'Internal Server Error' })
    }
  } else {
    res.setHeader('Allow', ['GET'])
    res.status(405).end(`Method ${req.method} Not Allowed`)
  }
}

export default authHandler
works like a charm now :) This is how _app.tsx looks with the ApolloProvider:
const App = ({ Component, pageProps }: AppProps) => {
  const viewport = useViewport()
  const [client, setClient] =
    useState<ApolloClient<NormalizedCacheObject> | null>(null)

...

  return (
    <>
      <Head>
        <title>myfairs | Management Suite</title>
      </Head>

      <ApolloProvider client={client}>
...
Thank you for the hints and advice
Sweet! Glad you got it working 💪