NextJS 14.2.4 With-Supabase Build Error - Error: `cookies` was called outside a request scope.
Verify canary release
- [X] I verified that the issue exists in the latest Next.js canary release
Provide environment information
Operating System:
Platform: darwin
Arch: arm64
Version: Darwin Kernel Version 23.5.0: Wed May 1 20:12:58 PDT 2024; root:xnu-10063.121.3~5/RELEASE_ARM64_T6000
Available memory (MB): 16384
Available CPU cores: 10
Binaries:
Node: 20.5.1
npm: 10.3.0
Yarn: 1.22.22
pnpm: N/A
Relevant Packages:
next: 14.2.4 // Latest available version is detected (14.2.4).
eslint-config-next: 13.0.0
react: 18.2.0
react-dom: 18.2.0
typescript: 5.3.3
Next.js Config:
output: N/A
Which example does this report relate to?
with-supabase
What browser are you using? (if relevant)
No response
How are you deploying your application? (if relevant)
next build
Describe the Bug
Running into a bug in my project. I'm trying test a build locally before building my app into a docker container. I continuing to get the same build error response:
▲ Next.js 14.2.4
- Environments: .env
Creating an optimized production build ...
✓ Compiled successfully
✓ Linting and checking validity of types
Collecting page data ..Error: `cookies` was called outside a request scope. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context
at getExpectedRequestStore (/Users/aesi-kylem/Documents/Gitlab/testing-build/node_modules/next/dist/client/components/request-async-storage.external.js:28:11)
at u (/Users/aesi-kylem/Documents/Gitlab/testing-build/.next/server/chunks/544.js:10:3754)
at a (/Users/aesi-kylem/Documents/Gitlab/testing-build/.next/server/app/page.js:1:4507)
at 1903 (/Users/aesi-kylem/Documents/Gitlab/testing-build/.next/server/app/page.js:1:4014)
at t (/Users/aesi-kylem/Documents/Gitlab/testing-build/.next/server/webpack-runtime.js:1:128)
at 9729 (/Users/aesi-kylem/Documents/Gitlab/testing-build/.next/server/app/page.js:1:1631)
at t (/Users/aesi-kylem/Documents/Gitlab/testing-build/.next/server/webpack-runtime.js:1:128)
at r (/Users/aesi-kylem/Documents/Gitlab/testing-build/.next/server/app/page.js:1:5582)
at /Users/aesi-kylem/Documents/Gitlab/testing-build/.next/server/app/page.js:1:5621
at t.X (/Users/aesi-kylem/Documents/Gitlab/testing-build/.next/server/webpack-runtime.js:1:1206)
> Build error occurred
Error: Failed to collect page data for /
at /Users/aesi-kylem/Documents/Gitlab/testing-build/node_modules/next/dist/build/utils.js:1268:15
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
type: 'Error'
}
Collecting page data .error: script "build" exited with code 1
I'm using the with-supabase template and this error is suggesting a mishandling of the cookies call which is giving the out of scope error. The error seems to bounce from the root / page and my /projects. Both are a fairly standard setup of a page.tsx server async component (default) awaiting fetches from a "use server" actions file that fetches from my supabase DB and passes the data down to client components as props.
Simplified Page.tsx minus the props client pass:
// app/page/page.tsx
import { getLatestProjects } from "@/libs/actions/actions";
async function Page() {
const projects = await getLatestProjects();
return (
<main className="flex flex-1 flex-col gap-4 p-4 md:gap-8 md:p-8 max-w-[90vw] mx-auto">
<ul className=" text-black">
{projects?.map((project) => (
<li key={project.id}>{project.name}</li>
))}
</ul>
</main>
);
}
export default Page;
actions.ts:
"use server"
import { createClient } from "@/utils/supabase/server";
const supabase = createClient();
export async function getLatestProjects() {
let { data: projects, error } = await supabase
.from('new_projects')
.select('*')
.order('end_date', { ascending: false })
.limit(10);
if (error) throw error;
return projects;
}
To troubleshoot I created a fresh new project using npx create-next-app -e with-supabase and copied over only my .env file and the simplified code you see above to test. When running next build i get the same error.
Expected Behavior
No error should be present when running next build with this project, especially, with no extra config changes to any of the boilerplate auth/session/cookie handling code in the with-supabase,
To Reproduce
-
Pull down or create a fresh Next app using the
with-supabaseexample:npx create-next-app -e with-supabase -
Rename the
.env.exampleto.envand provide yourNEXT_PUBLIC_SUPABASE_URLandNEXT_PUBLIC_SUPABASE_ANON_KEY
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
- Create a table with some dummy data of at least
idandname. Create an actions file in/lib/actions/actions.ts. Paste the below fetch using the supabase client to your dummy data:
import { createClient } from "@/utils/supabase/server";
const supabase = createClient();
export async function getTest() {
let { data: test, error } = await supabase
.from('**your_db**')
.select('*')
.limit(10);
if (error) throw error;
return test;
}
- Remove all boilerplate content from
app/page.tsxand replace with the following:
// app/page/page.tsx
import { getTest } from "@/libs/actions/actions";
async function Page() {
const tests = await getTest();
return (
<main className="flex flex-1 flex-col gap-4 p-4 md:gap-8 md:p-8 max-w-[90vw] mx-auto">
<ul className=" text-black">
{tests?.map((test) => (
<li key={test.id}>{test.name}</li>
))}
</ul>
</main>
);
}
export default Page;
- Test that the app runs with
next devand that your dummy data shows on page athttp://localhost:3000/ - Finally, run
next build, this is where it will error our citingCollecting page data ..Error:cookieswas called outside a request scope. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context
I am also facing same issue, have you been able to resolve this problem?
hmm i am also getting the same error when using supabase/server.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options),
)
},
},
},
)
}
It happens when i am making a stripe webhook call and want to save user's data in supabase, it is working fine it i directly use supabase
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
)
I have encountered the same problem. What is strange this problem occurs with me only on a specific route. On other route pages where I have basically almost identical logic the problem does not occur.
Interestingly - With the same code, a week ago, everything worked. Isn't this a problem maybe with Supabase?
I'm using Next 14.2.3
Problem occurs on local server - npm run dev
I was having a similar issue, I was able to fix it by ensuring that my createClient() statements are within the functions of my code. For example, your actions.ts should be updated to the below:
Actions.ts
"use server"
import { createClient } from "@/utils/supabase/server";
export async function getLatestProjects() {
const supabase = createClient();
let { data: projects, error } = await supabase
.from('new_projects')
.select('*')
.order('end_date', { ascending: false })
.limit(10);
if (error) throw error;
return projects;
}
I have encountered the same problem. What is strange this problem occurs with me only on a specific route. On other route pages where I have basically almost identical logic the problem does not occur.
Interestingly - With the same code, a week ago, everything worked. Isn't this a problem maybe with Supabase?
I'm using Next
14.2.3Problem occurs on local server -npm run dev
Interesting, I haven't had the issue in dev so far, just the build step. Which I needed to work in order to build a docker container for hosting but also would assume I would get errors if I pushed to Vercel or others.
I'm on 14.2.4 but when i started this project I think it was 14.2.1. As well, I have used both NPM and Bun, with and without turbopack enabled.
I was having a similar issue, I was able to fix it by ensuring that my createClient() statements are within the functions of my code. For example, your actions.ts should be updated to the below:
Actions.ts
"use server" import { createClient } from "@/utils/supabase/server"; export async function getLatestProjects() { const supabase = createClient(); let { data: projects, error } = await supabase .from('new_projects') .select('*') .order('end_date', { ascending: false }) .limit(10); if (error) throw error; return projects; }
Hmm, this makes sense. Prior to posting here I did run a bunch of google searches and the only thread or mention I could find did talk about making sure my cookies() call was inside my function. But I was not directly using that anywhere in my app. However I didn't think about that pass down of the cookies that are called in the supabase client.
I will give this a shot, I did already find a workaround of sorts. I just moved all the calls to my Page.tsx component rather than refactoring to the actions file. This works but was still puzzled as to why a pattern I have used everywhere else seemingly was not allowed?
Thanks for the suggestion! I will update if that works for me.
Ok I also fixed my problem
Here is how my action.ts looks like now
"use server";
import { createClient } from "@/utils/supabase/server";
export async function getAllClients() {
const supabase = createClient();
const { data } = await supabase.from("profiles").select();
return data ?? [];
}
export async function getSpecificClient(clientId: string) {
const supabase = createClient();
const { data } = await supabase
.from("profiles")
.select()
.eq("id", clientId)
.single();
return data;
}
In a previous version of this file, I declared const supabase = createClient() once right after importing the file.
Unfortunately, the initialization of the supabase has to be done separately inside each exported function. I don't like such duplication of code 😢
can confirm that the problem is in fact usage of createClient() out of function scope aka usage as global variable.
yea the soln is just to find any where you called createClient that is not inside a function , dont call your createClient in a global scope
How does it work with unit testing though? How do you unit test those functions, if cookies aren't available when running unit test with e.g. jest?
How does it work with unit testing though? How do you unit test those functions, if cookies aren't available when running unit test with e.g. jest?
Mock them, i guess.
Anyone knows if there is a solution for creating a global instance of the client yet?
What if you want to use unstable_cache which doesn't allow the client (using cookies) to be initialized inside the function?
I found a workaround and thought maybe someone would appreciate it.
getCachedNotifications is just unstable_cache wrapped around the logic for getting the notifications. What is important is to not initialize supabase ssr client inside the cache function as the supabase ssr client uses cookies (user data) - which we don't want to cache.
export const getNotificationsAction = async ({
archived = false,
}: {
archived?: boolean
}) => {
const supabase = createSupabaseServerClient()
const { data, error } = await supabase.auth.getUser()
if (error || !data.user) {
throw new Error('You must be signed in to perform this action')
}
return getCachedNotifications(supabase, data.user.id, archived)
}
I was having a similar issue, I was able to fix it by ensuring that my createClient() statements are within the functions of my code. For example, your actions.ts should be updated to the below:
Actions.ts
"use server" import { createClient } from "@/utils/supabase/server"; export async function getLatestProjects() { const supabase = createClient(); let { data: projects, error } = await supabase .from('new_projects') .select('*') .order('end_date', { ascending: false }) .limit(10); if (error) throw error; return projects; }
@robskinney Thanks, this solution worked. The problem was that when we initialise supabase client outside the scope of a function is that it doesn't have access to cookies cause cookies aren't available outside of an http request. Next.js collects static page data when building the project and since cookies are not available at build-time, it throws an error.
Make sure to only initialise this in a function or scope where it has access to cookies, otherwise this error would be thrown.
An error message appears:
Error: cookies was called outside a request scope. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context
Environment: Next.js 14 with Supabase.
To account for scalability, I'm using the Supabase client as shown below.
Service.ts
import { Database } from '@/types/supabase';
import { createSupabaseClient as createSupabaseClientUtil } from '@/utils/supabase/createClient';
import { SupabaseClient } from '@supabase/supabase-js';
// Differentiating between server or client based on runtime environment
abstract class Service {
protected supabase: SupabaseClient<Database>;
constructor() {
this.supabase = createSupabaseClientUtil();
}
}
export default Service;
utils/supabase/createClient.ts
import { createClient } from '@supabase/supabase-js';
export function createSupabaseClient() {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
if (typeof window === 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { headers, cookies } = require('next/headers');
return createClient(supabaseUrl, supabaseKey, {
auth: {
persistSession: false,
detectSessionInUrl: false
},
global: {
headers: {
...Object.fromEntries(headers().entries()),
Cookie: cookies().toString()
}
}
});
} else {
return createClient(supabaseUrl, supabaseKey);
}
}
createClient.ts
import { createClient } from '@supabase/supabase-js';
import { createServerClient, type CookieOptions } from '@supabase/ssr';
import { Database } from '@/types/supabase';
export function createSupabaseClient() {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
if (typeof window === 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { cookies } = require('next/headers');
const cookieStore = cookies();
return createServerClient<Database>(supabaseUrl, supabaseKey, {
cookies: {
get(name: string) {
return cookieStore.get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
try {
cookieStore.set({ name, value, ...options });
} catch (error) {
// Ignored if `set` is called from a Server Component.
}
},
remove(name: string, options: CookieOptions) {
try {
cookieStore.set({ name, value: '', ...options });
} catch (error) {
// Ignored if `delete` is called from a Server Component.
}
}
}
});
} else {
return createClient<Database>(supabaseUrl, supabaseKey);
}
}
I use it as follows:
AdminInstructor-service.ts
import Service from '@/service/Service';
import { MEMBERS_PER_PAGE } from '@/utils/constants';
class AdminInstructorService extends Service {
// Retrieve all instructors
public async all(pageParam: number) {
if (pageParam > 1) {
await new Promise(resolve => setTimeout(resolve, 500));
}
const start = (pageParam - 1) * MEMBERS_PER_PAGE;
const end = start + MEMBERS_PER_PAGE - 1;
const { data, count, error } = await this.supabase
.from('instructors')
.select('*', { count: 'exact' })
.order('created_at', { ascending: false })
.range(start, end);
const totalPage = count ? Math.ceil(count / MEMBERS_PER_PAGE) : 0;
if (error) throw error;
return {
instructors: data,
page: pageParam,
totalPage
};
}
// Create a new instructor
public async createInstructor({ memberId, username }: { memberId?: string; username: string }) {
if (memberId) {
this.isInstructor(memberId); // Checks if the user is already an instructor
}
const { data: instructorData, error: instructorError } = await this.supabase
.from('instructors')
.insert({ instructor_name: username })
.select('id, instructor_name')
.single();
if (instructorError) throw instructorError;
const instructorId = instructorData.id;
// If only a new instructor is created, memberId will be missing, so this part is skipped.
if (memberId) {
const { error: updateError } = await this.supabase
.from('members')
.update({ instructor_id: instructorId })
.eq('id', memberId);
if (updateError) throw updateError;
}
}
public async deleteInstructor({ instructorId }: { instructorId: string }) {
const { error } = await this.supabase.from('instructors').delete().eq('id', instructorId);
if (error) throw error;
}
// Checks if already an instructor
public async isInstructor(memberId: string) {
const { data: memberData, error: memberError } = await this.supabase
.from('members')
.select('id, username, instructor_id')
.eq('id', memberId)
.single();
if (memberError || !memberData) {
throw new Error('404');
} else if (memberData.instructor_id) {
throw new Error('409');
}
}
}
// eslint-disable-next-line import/no-anonymous-default-export
export default new AdminInstructorService();
When I use React Query hydration in a server component, I get an error. What could be the cause?
hmm i am also getting the same error when using
supabase/server.tsimport { createServerClient, type CookieOptions } from '@supabase/ssr' import { cookies } from 'next/headers' export function createClient() { const cookieStore = cookies() return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { getAll() { return cookieStore.getAll() }, setAll(cookiesToSet) { cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options), ) }, }, }, ) }It happens when i am making a stripe webhook call and want to save user's data in supabase, it is working fine it i directly use supabase
import { createClient } from '@supabase/supabase-js' const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, )
Same here. When using supabase/server.ts from official doc, impossible to use generateStaticParams().
And working with createclient from @supabase/supabase-js works, but is it ok to work this way ?
just wanted to share something else, might help someone
In my case the client was already being created within a function
import { env } from '@/env';
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
env.NEXT_PUBLIC_SUPABASE_URL,
env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options),
);
} catch {
// The `setAll` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
},
);
}
pretty standard, got it from the tutorial at https://supabase.com/docs/guides/auth/server-side/nextjs
Now, in my page I had the following
'use client';
import { SignUpForm } from "@/app/sign-up/signup-form";
export default function SignUpPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="w-full max-w-md p-8 space-y-4 bg-card rounded-lg shadow-lg">
<SignUpForm />
</div>
</div>
);
}
but it wouldn't work, got the error in the title
the key was removing the 'use client' directive
Hey! Thanks for the tips :)
On Mon, Jan 20, 2025 at 7:27 AM Gaspar Dip @.***> wrote:
just wanted to share something else, might help someone else
In my case the client was already being created within a function
import { env } from '@/env';import { createServerClient } from @.***/ssr';import { cookies } from 'next/headers'; export async function createClient() { const cookieStore = await cookies();
return createServerClient( env.NEXT_PUBLIC_SUPABASE_URL, env.NEXT_PUBLIC_SUPABASE_ANON_KEY, { cookies: { getAll() { return cookieStore.getAll(); }, setAll(cookiesToSet) { try { cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options), ); } catch { // The
setAllmethod was called from a Server Component. // This can be ignored if you have middleware refreshing // user sessions. } }, }, }, );}pretty standard, got it from the tutorial at https://supabase.com/docs/guides/auth/server-side/nextjs
Now, in my page I had the following
'use client'; import { SignUpForm } from "@/app/sign-up/signup-form"; export default function SignUpPage() { return ( <div className="flex min-h-screen items-center justify-center"> <div className="w-full max-w-md p-8 space-y-4 bg-card rounded-lg shadow-lg"> <SignUpForm /> );}
but it wouldn't work, got the error in the title
the key was removing the 'use client' directive
— Reply to this email directly, view it on GitHub https://github.com/vercel/next.js/issues/67191#issuecomment-2601470193, or unsubscribe https://github.com/notifications/unsubscribe-auth/A5LSZ76T3E2J3TFAIMNSU7L2LSJL7AVCNFSM6AAAAABJ4K3OYWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDMMBRGQ3TAMJZGM . You are receiving this because you commented.Message ID: <vercel/next. @.***>
It seems that this bug is still present. I'm using the exact same code than the with-supabase example and when i press the Sign out button, this bugs shows up and i'm unable to sign out from my account. https://github.com/vercel/next.js/tree/canary/examples/with-supabase All of my createClient() calls are inside functions...
@BodaThomas , can you share some code? Maybe we will be able to figure out something
I had this same issue and wanted to share how I was able to resolve it in my project. I had multiple security headers being set in my project root middleware.ts. I found that Next.js server actions require certain inline scripts to function properly and that my middleware was restricting the functionality causing the cookies was called outside a request scope error.
I changed my middleware to this which fixed it on my end:
import { type NextRequest } from "next/server";
import { updateSession } from "@/utils/supabase/middleware";
export async function middleware(request: NextRequest) {
const res = await updateSession(request);
// Security headers
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'nonce-${nonce}' 'unsafe-inline';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
block-all-mixed-content;
upgrade-insecure-requests;
`;
res.headers.set(
"Content-Security-Policy",
cspHeader.replace(/\s{2,}/g, " ").trim()
);
res.headers.set("X-Frame-Options", "DENY");
res.headers.set("X-Content-Type-Options", "nosniff");
res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
res.headers.set(
"Permissions-Policy",
"camera=(), microphone=(), geolocation=()"
);
return res;
}
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};
My key change was adding 'unsafe-eval' and 'unsafe-inline' to script-src. Probably a different way to do this but this is what I found worked on my end.
It seems that this bug is still present. I'm using the exact same code than the with-supabase example and when i press the Sign out button, this bugs shows up and i'm unable to sign out from my account. https://github.com/vercel/next.js/tree/canary/examples/with-supabase All of my createClient() calls are inside functions...
Just like you, all my calls to the createClient function are inside a function, but the issue persist:
export const loginAction = async (formData: FormData) => {
const supabase = await createClient()
const email = formData.get('email') as string
const password = formData.get('password') as string
const { error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) {
console.error('Login error:', error.message)
redirect(`/auth/login?error=${encodeURIComponent(error.message)}`)
}
return redirect('/')
}
"dependencies": {
"@supabase/ssr": "^0.5.2",
"@supabase/supabase-js": "^2.48.1",
"next": "15.1.7",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
EDIT: My custom matcher missed some static assets, triggering redirects to /auth/login. This led to a syntax error in the browser console and cookie errors outside the request scope on the server. The simpler matcher from the docs fixed both by correctly excluding assets.