Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

getServerSession is null in Next.js API routes (within the app directory) #7423

Closed
ghoshnirmalya opened this issue May 1, 2023 · 18 comments
Closed
Labels
triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime.

Comments

@ghoshnirmalya
Copy link
Contributor

Environment

System:
  OS: macOS 13.3.1
  CPU: (8) arm64 Apple M2
  Memory: 95.55 MB / 8.00 GB
  Shell: 5.9 - /bin/zsh
Binaries:
  Node: 19.2.0 - ~/.nvm/versions/node/v19.2.0/bin/node
  Yarn: 1.22.19 - ~/.nvm/versions/node/v19.2.0/bin/yarn
  npm: 9.6.2 - ~/.nvm/versions/node/v19.2.0/bin/npm
Browsers:
  Chrome: 112.0.5615.137
  Safari: 16.4

Reproduction URL

https://github.com/ghoshnirmalya/the-fullstack-app

Describe the issue

The following code always returns null if I do console.log(session) when the code is present inside the Next.js API Routes within the app directory:

export async function GET() {
  try {
    const session = await getServerSession(authOptions);
    ..
}

You can find the relevant code here.

However, the following code from the React Server Components returns the correct data:

export default async function ProjectIndexPage() {
  const session = await getServerSession(authOptions);
  ..
  
  return (..)
}

The session in the above case is something like the following:

{
  user: {
    name: 'John Doe',
    email: 'john@doe.com',
    image: 'https://lh3.googleusercontent.com/john-doe',
    id: '12345678910'
  }
}

You can find the relevant code here.

How to reproduce

  1. Clone the repository:
    git clone git@github.com:ghoshnirmalya/the-fullstack-app.git
  2. Install the necessary dependencies:
    pnpm install
  3. Add the necessary env vars:
    DATABASE_URL='mysql://database-url'
    NEXT_PUBLIC_VERCEL_URL=127.0.0.1:3000
    NEXTAUTH_SECRET=some-secret
    NEXTAUTH_URL=http://127.0.0.1:3000
    GOOGLE_CLIENT_ID=google-client-id
    GOOGLE_CLIENT_SECRET=google-client-secret
  4. Generate the Prisma client:
    npx prisma db push && npx prisma generate
  5. Run the development server:
    pnpm run dev

Expected behavior

The session should return the correct object from the API Routes. The console.log(session) should return something like the following:

{
  user: {
    name: 'John Doe',
    email: 'john@doe.com',
    image: 'https://lh3.googleusercontent.com/john-doe',
    id: '12345678910'
  }
}
@ghoshnirmalya ghoshnirmalya added the triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime. label May 1, 2023
@freshtechs
Copy link

freshtechs commented May 2, 2023

Im also seeing this behaviour when fetching (fetch(http://localhost:3000/api/hello)) from a React Server Component to any api route.ts.
But if full url is manually entered in browser, the getServerSession returns a correct value. I deleted my middleware.ts trying to debug without success.

@balazsorban44
Copy link
Member

balazsorban44 commented May 2, 2023

This is expected, based on your reproduction.

The issue in your case (and likely the same for your @freshtechs) is that you are invoking a Route Handler via fetch from a Server Component. By default, cookies are not passed in a server-side fetch call for security reasons (to avoid accidentally attaching cookies to third-party API calls). You have 2 options:

  1. Don't use a Route Handler: Since you are already in a server context and just reading some data, you can import the function that returns the data directly.
  2. Make sure to pass the headers to the fetch call:
import { headers } from "next/headers"

// ...
const response = await fetch(getApiUrl("api/projects"), {
  method: "GET",
  headers: headers()
})

In your case, option 1 makes more sense.

@ghoshnirmalya
Copy link
Contributor Author

@balazsorban44 Thanks so much for the reply.

I can confirm that I tried the option 2 and it works fine. However, I'm getting the following TypeScript error:

Type 'ReadonlyHeaders' is not assignable to type 'HeadersInit | undefined'.
  Type 'ReadonlyHeaders' is missing the following properties from type 'Headers': append, delete, set

Screenshot 2023-05-02 at 2 41 15 PM

I'm guessing that this error arises because the Next.js headers are only read-only for now. Any way in which I can resolve this other than ignoring it?

Regarding option 1, do you recommend that I use Prisma directly in the server components like the following instead of the API Route?

export default async function ProjectIndexPage() {
-  const response = await fetch(getApiUrl("api/projects"), {
-    method: "GET",
-    cache: "no-store",
-    headers: headers(),
-  });
-  const projects: Project[] = await response.json();

+  const projects = await prisma.project.findMany({
+    where: {
+      creatorId: "1",
+    },
+  });

  return (..)
}

@balazsorban44
Copy link
Member

balazsorban44 commented May 2, 2023

Thinking ahead of you. 😉 vercel/next.js#49075

My recommendation would be to just extract the Route Handler into a function like getProjects(session) or similar for convenience. Note, you can have that in a separate file if you need to, and have it next to the page. (Colocation is one of the benefits of using App Router architecture IMO).

If you worry about performance, check out https://beta.nextjs.org/docs/data-fetching/fetching#data-fetching-without-fetch and https://beta.nextjs.org/docs/data-fetching/caching#per-request-caching

@freshtechs
Copy link

Wonderful @balazsorban44 thanks for clarifying ✅

@jerodfritz
Copy link

fwiw, I can retrieve auth state in a route handler using a CredentialsProvider. Here is my /game/[...path]/route.ts that passes on requests. This works on vercel w/ next 13.4.4 and next-auth 4.22.1

#7545

import { NextRequest, NextResponse } from "next/server"
import { getServerSession } from "next-auth"

import { authOptions } from "../../auth/[...nextauth]/route"

const getToken = async () => {
  const session: any = await getServerSession(authOptions)
  let token
  if (session && session.jwt) {
    token = session.jwt
  }
  return token
}

export async function GET(req: NextRequest, res: NextResponse) {
  const token = await getToken()
  if (!token) {
    return new NextResponse("Unauthorized", { status: 403 })
  }
  const path = req.nextUrl.pathname.replace("/game", "")
  const response = await fetch(`${process.env.API_URL}${path}/`, {
    headers: {
      Authorization: `Bearer ${token}`,
    },
    cache: "no-store",
  })

  const result = await response.json()

  if (response.ok) {
    try {
      return NextResponse.json(result)
    } catch (error) {
      console.log("error parsing response", error)
      return NextResponse.json(undefined)
    }
  } else {
    return NextResponse.json({
      error: result.error?.message || "An unknown error occurred",
    })
  }
}

export async function POST(request: NextRequest) {
  const token = await getToken()
  if (!token) {
    return new NextResponse("Unauthorized", { status: 403 })
  }

  const body = JSON.stringify(await request.json())

  const path = request.nextUrl.pathname.replace("/game", "")

  const response = await fetch(`${process.env.API_URL}${path}/`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${token}`,
    },
    body,
  })

  const result = await response.json()

  if (response.ok) {
    try {
      return NextResponse.json(result)
    } catch (error) {
      console.log("error parsing response", error)
      return NextResponse.json(undefined)
    }
  } else {
    return NextResponse.json({
      error: result.error?.message || "An unknown error occurred",
    })
  }
}

@martinso95
Copy link

Thinking ahead of you. 😉 vercel/next.js#49075

My recommendation would be to just extract the Route Handler into a function like getProjects(session) or similar for convenience. Note, you can have that in a separate file if you need to, and have it next to the page. (Colocation is one of the benefits of using App Router architecture IMO).

If you worry about performance, check out https://beta.nextjs.org/docs/data-fetching/fetching#data-fetching-without-fetch and https://beta.nextjs.org/docs/data-fetching/caching#per-request-caching

Hi @balazsorban44

Are there any downsides with using route handler to wrap external api:s, such as prisma or firebase? versus just having a separate function as you said. Any performance differences?

I am thinking that, if i use route handler to wrap my firebase data getter, i can then simply treat this as a normal endpoint and use fetch with its features such as cache config. or is going to cause cache issues and simply not going to work?

I know it is possible to use cache(), revalidate, etc. as you linked. But I feel like using fetch is the prioritized way, and also more straight forward and clean.

@balazsorban44
Copy link
Member

balazsorban44 commented Jul 10, 2023

fetch is adding an extra network call, another point of failure, so, both performance and stability is affected. Next.js should provide with the necessary APIs to cache the request properly, even if it's using a database like Firebase. You can learn more about caching at https://nextjs.org/docs/app/building-your-application/data-fetching/caching

@martinso95
Copy link

makes sense, thanks!

@felipe-ff
Copy link

felipe-ff commented Jul 19, 2023

@balazsorban44 I have a simple project firebase + next-auth app, it works perfectly signin in with the GoogleProvider, getServerSession(authOptions) gives me the session correctly, but when using CredentialsProvider it is null, I'm not using fetch anywhere, I'm just trying to get the serversession from within the app folder root page.tsx, here is my auth file:

import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import CredentialsProvider from 'next-auth/providers/credentials';
import { UserCredential, getAuth, signInWithEmailAndPassword } from 'firebase/auth';
import { FirestoreAdapter } from '@auth/firebase-adapter';
import { cert } from 'firebase-admin/app';
import { Adapter } from 'next-auth/adapters';
import { auth } from '@/app/firebase';
export const authOptions = {
  pages: {
    signIn: '/login',
  },
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      credentials: {},
      async authorize(credentials: any): Promise<any> {
        return await signInWithEmailAndPassword(auth, credentials.email || '', credentials.password || '')
          .then((userCredential) => {
            console.log('USER CREDENTIAL', !!userCredential.user);
            if (userCredential.user) {
              return userCredential.user;
            }
            return null;
          })
          .catch((error) => console.log(error));
      },
    }),
    GoogleProvider({
      clientId: 'xxxx',
      clientSecret: 'xxxx',
    }),
  ],
  adapter: FirestoreAdapter({
    credential: cert({
      projectId: process.env.FIREBASE_PROJECT_ID,
      clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
      privateKey: process.env.FIREBASE_PRIVATE_KEY!.replace(/\\n/g, '\n'),
    }),
  }) as Adapter,
  callbacks: {
    async session(session: any) {
      console.log('ses',session);
      return session;
    },

    async signIn(profile: any) {
      console.log('profile', profile);
      return profile;
    },
  },
  secret: process.env.NEXT_PUBLIC_SECRET,
};
export default NextAuth(authOptions);

Any idea why? also the adapter only works with GoogleProvider.

The console.log('ses', session) is always null when using Credential provider.

@dellwatson
Copy link

@felipe-ff same issue, i did following this for credentials login:
#4394 (reply in thread)

using database strategy, but it seems the adapter.getSession is currently problematic for firebase adapter.

@0xHorace
Copy link

0xHorace commented Aug 3, 2023

I can't even import getServerSession: "Cannot find module 'next-auth/next' or its corresponding type declarations."

has anyone else encountered this? I am so confused

@PratikDev
Copy link

This is expected, based on your reproduction.

The issue in your case (and likely the same for your @freshtechs) is that you are invoking a Route Handler via fetch from a Server Component. By default, cookies are not passed in a server-side fetch call for security reasons (to avoid accidentally attaching cookies to third-party API calls). You have 2 options:

  1. Don't use a Route Handler: Since you are already in a server context and just reading some data, you can import the function that returns the data directly.
  2. Make sure to pass the headers to the fetch call:
import { headers } from "next/headers"

// ...
const response = await fetch(getApiUrl("api/projects"), {
  method: "GET",
  headers: headers()
})

In your case, option 1 makes more sense.

Hi @balazsorban44. Can you please give an example for option 1? I'm calling the getServerSession function in one of my SSR page, but it's returning null. I'm using google login. My code looks like,

import { authOptions } from "@/lib/auth";
import { getServerSession } from "next-auth";

const page = async ({}) => {
  const session = await getServerSession(authOptions);

  return (
    <>
      <pre>{JSON.stringify(session)}</pre>
    </>
  );
};

export default page;

I can see the below error in the server console,

[next-auth][error][JWT_SESSION_ERROR] 
https://next-auth.js.org/errors#jwt_session_error Cannot read properties of undefined (reading 'id') {
  message: "Cannot read properties of undefined (reading 'id')",
  stack: "TypeError: Cannot read properties of undefined (reading 'id')\n" +
    '    at Object.jwt (webpack-internal:///(rsc)/./lib/auth.ts:29:87)\n' +
    '    at Object.session (webpack-internal:///(rsc)/./node_modules/next-auth/core/routes/session.js:30:43)\n' +
    '    at async AuthHandler (webpack-internal:///(rsc)/./node_modules/next-auth/core/index.js:161:37)\n' +
    '    at async getServerSession (webpack-internal:///(rsc)/./node_modules/next-auth/next/index.js:125:21)\n' +
    '    at async page (webpack-internal:///(rsc)/./app/dashboard/page.tsx:14:21)',
  name: 'TypeError'
}

let me know if you need more info

@Rrhapsod
Copy link

Rrhapsod commented Oct 3, 2023

This is expected, based on your reproduction.

The issue in your case (and likely the same for your @freshtechs) is that you are invoking a Route Handler via fetch from a Server Component. By default, cookies are not passed in a server-side fetch call for security reasons (to avoid accidentally attaching cookies to third-party API calls). You have 2 options:

1. Don't use a Route Handler: Since you are already in a server context and just reading some data, you can import the function that returns the data directly.

2. Make sure to pass the headers to the fetch call:
import { headers } from "next/headers"

// ...
const response = await fetch(getApiUrl("api/projects"), {
  method: "GET",
  headers: headers()
})

In your case, option 1 makes more sense.

It worked in dev mode, but not in production. Vercel is showing me this error log:

[Error]: Headers cannot be modified. Read more: https://nextjs.org/docs/app/api-reference/functions/headers

@devrsi0n
Copy link

I worked around this by getting jwt token instead in RSC,

const jwt = await getToken({
    // `getToken` only need these attributes, to make it compatible with app router
    // This code is copied from `getServerSession`
    req: {
      headers: Object.fromEntries(headers() as Headers),
      cookies: Object.fromEntries(
        cookies()
          .getAll()
          .map((c) => [c.name, c.value])
      )
    } as unknown as NextRequest
  });

@fsolarv22
Copy link

fsolarv22 commented Oct 28, 2023

Hello, I'm having a similar problem with server components using Next Auth with the Firebase Auth service.

Error: Error: Cannot destructure property 'user' of '(intermediate value)' as it is null.

Server component:

export default async function RSC() {
  const { user } = await getAuthSession()

  return (
    <>
      
    </>
  )
}

On the client, once the user is authenticated I pass the firebase token to the signIn() function.

I'm exporting the getServerSession in a separate auth-option.js file.

import CredentialsProvider from "next-auth/providers/credentials"
import { auth as serverAuth } from "@/lib/firebase-admin"
import { getServerSession } from "next-auth"

export const authOptions = {
  providers: [
    CredentialsProvider({
      credentials: {},
      authorize: async ({ idToken }, _req) => {
        if (idToken) {
          try {
            const decoded = await serverAuth.verifyIdToken(idToken)

            return { ...decoded }
          } catch (error) {
            throw new Error(error)
          }
        }
        return null
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      let t = {}
      let u = {}
      if (token) t = token
      if (user) u = user
      return { ...t, ...u }
    },

    async session({ session, token }) {
      session.user.emailVerified = token.emailVerified
      session.user.uid = token.uid
      return session
    },
  },
  session: {
    strategy: "jwt",
    maxAge: 12 * 60 * 60 * 180,
  },
  secret: process.env.NEXTAUTH_SECRET,
}

export const getAuthSession = () => getServerSession(authOptions)

The code was working fine and suddenly stopped working. Same behavior with Next 13 and 14. useSession() function works fine.

PS: I couldn't get the firebase adapter to work and I don't get any errors so I can't debug.

@alantoledo007
Copy link

This is expected, based on your reproduction.
The issue in your case (and likely the same for your @freshtechs) is that you are invoking a Route Handler via fetch from a Server Component. By default, cookies are not passed in a server-side fetch call for security reasons (to avoid accidentally attaching cookies to third-party API calls). You have 2 options:

1. Don't use a Route Handler: Since you are already in a server context and just reading some data, you can import the function that returns the data directly.

2. Make sure to pass the headers to the fetch call:
import { headers } from "next/headers"

// ...
const response = await fetch(getApiUrl("api/projects"), {
  method: "GET",
  headers: headers()
})

In your case, option 1 makes more sense.

It worked in dev mode, but not in production. Vercel is showing me this error log:

[Error]: Headers cannot be modified. Read more: https://nextjs.org/docs/app/api-reference/functions/headers

  • Change headers() -> new Headers(headers())

@paschalidi
Copy link

Changing headers() -> new Headers(headers()) worked! 🙇

@balazsorban44 balazsorban44 unpinned this issue Jan 25, 2024
joshuawootonn added a commit to joshuawootonn/type-the-word that referenced this issue Jun 30, 2024
Next Auth uses cookies. Request from a server component strip cookies. So I moved the db call into the server component.

Helpful link:
nextauthjs/next-auth#7423 (comment)
joshuawootonn added a commit to joshuawootonn/type-the-word that referenced this issue Jun 30, 2024
Next Auth uses cookies. Request from a server component strip cookies. So I moved the db call into the server component.

Helpful link:
nextauthjs/next-auth#7423 (comment)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime.
Projects
None yet
Development

No branches or pull requests