One import, one runtime environment mismatch, one confusing error. Here is the specific thing that trips up many developers when adding authentication to a Next.js App Router application.
If you have added authentication to a Next.js App Router application using Server Components, you have probably used cookies() from next/headers:
import { cookies } from "next/headers";
export default async function Page() {
const cookieStore = cookies();
const session = cookieStore.get("sa_session");
// ...
}
This works perfectly in Server Components and Route Handlers. The natural instinct when writing middleware is to use the same pattern. It will not work — and the error you get is not immediately helpful.
THE TWO COOKIE APIS
Next.js has two separate cookie APIs for two separate execution contexts.
cookies() from next/headers — Available in Server Components, Route Handlers, and Server Actions. Reads the incoming request's cookies via the Node.js request context. Throws if called outside a request context or in the Edge runtime.
request.cookies.get() from NextRequest — Available in middleware. Reads cookies directly from the incoming request object passed to the middleware function. Works in both the Edge runtime and Node.js.
Both give you cookie values. Both are async-compatible. They are not interchangeable.
WHY MIDDLEWARE USES A DIFFERENT API
Next.js middleware runs in the Edge runtime — a lightweight JavaScript environment based on the Web Fetch API, Cloudflare Workers, and browser standards. It does not have Node.js APIs.
cookies() from next/headers is implemented using Node.js's AsyncLocalStorage to read the current request context. AsyncLocalStorage is a Node.js API. It does not exist in the Edge runtime.
When middleware runs, there is no Node.js request context to read from. There is only the NextRequest object passed as a parameter to the middleware function. That object has a cookies property that reads cookies from the request headers directly — no Node.js APIs needed, works in any runtime.
THE CORRECT WAY TO READ COOKIES IN MIDDLEWARE
In middleware, always read cookies from request.cookies:
// frontend/src/middleware.ts
import { NextRequest, NextResponse } from "next/server";
export default async function middleware(request: NextRequest) {
// Correct: read from the request object
const token = request.cookies.get("sa_session")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.nextUrl));
}
return NextResponse.next();
}
request.cookies.get("name") returns a RequestCookie | undefined. The ?.value extracts the string value or returns undefined if the cookie is absent.
To set or delete cookies in middleware, use response.cookies:
const response = NextResponse.next();
response.cookies.set("name", "value", { httpOnly: true });
// or to delete:
response.cookies.set("name", "", { maxAge: 0 });
return response;
THE ERROR YOU GET IF YOU USE THE WRONG ONE
If you import and call cookies() from next/headers inside middleware, the error is not obvious. You will see something like:
Error: cookies() expects to be called in a Server Component or Server Action
Or, depending on the Next.js version and runtime configuration:
Error: Invariant: Method expects to have requestAsyncStorage,
none available
Neither error message says "you are in the Edge runtime and cannot use Node.js APIs". They say the request context is unavailable, which is technically correct but not immediately actionable if you do not know about the runtime distinction.
WHEN EACH API IS AVAILABLE
| Context | cookies() from next/headers |
request.cookies from NextRequest |
|---|---|---|
| Server Component | ✅ | ✗ |
| Route Handler | ✅ | ✅ (via request param) |
| Server Action | ✅ | ✗ |
| Middleware | ✗ | ✅ |
| Client Component | ✗ | ✗ |
The pattern to remember:
- You have a
requestparameter → userequest.cookies - You are in a Server Component with no
request→ usecookies()fromnext/headers - Middleware always has a
requestparameter → always userequest.cookies
THE TAKEAWAY
The distinction exists because middleware runs in the Edge runtime and the other contexts run in Node.js. The Edge runtime does not have AsyncLocalStorage, which is how cookies() from next/headers works. The solution is a single line change: use request.cookies.get() instead of importing cookies().
This comes up every time someone builds authentication in Next.js App Router. The cookie read in the middleware must use request.cookies. Everywhere else, cookies() from next/headers is fine.
The ShopAgent demo is live at https://shop-agent.agilecreativeminds.nl. See the demo showcase or follow the demo walkthrough. Built by Agile Creative Minds.