Next.js 用 cookies()/headers() 读请求信息:动态渲染触发、缓存失效与在 Server Action 里读写 cookie
在 Next.js App Router 里想读一下用户的登录 token,或者拿一下 User-Agent,你会发现 Server Component 里根本没有 req 对象可用。官方给的入口是 next/headers 里的 cookies() 和 headers()。但很多人第一次用就踩坑:要么报「Dynamic server usage」构建失败,要么在 Server Component 里 cookies().set() 直接抛错。这篇把这两个函数的正确用法和背后的渲染机制讲透。
基本用法:在 Server Component 里读 cookie 和 header
cookies() 和 headers() 只能在服务端运行的代码里调用------Server Components、Server Actions、Route Handlers。注意 Next.js 15 起它们是异步 的,必须 await。
tsx
// app/dashboard/page.tsx ------ 这是一个 Server Component
import { cookies, headers } from 'next/headers'
export default async function Dashboard() {
const cookieStore = await cookies()
const token = cookieStore.get('token')?.value // 读单个 cookie
const headerList = await headers()
const ua = headerList.get('user-agent') ?? 'unknown' // 读请求头
return (
<div>
<p>登录态:{token ? '已登录' : '未登录'}</p>
<p>你的浏览器:{ua}</p>
</div>
)
}
如果你还在用 Next.js 14,cookies()/headers() 是同步的,不用 await。升级到 15 之后忘了加 await 会得到一个 Promise 而不是数据,.get is not a function 就是这么来的。
关键机制:调用它们会让整个路由变成动态渲染
这是最容易忽略、又最影响性能的一点。Next.js 默认会尽量把页面静态化 (构建时生成 HTML,请求时直接吐)。但 cookie 和 header 是每个请求都不一样的东西,一旦你在某个页面调用了 cookies() 或 headers(),Next.js 就知道这个页面没法静态化了,会自动切成动态渲染(每次请求实时生成)。
tsx
// 这个页面本来可以静态化,但因为读了 cookie,整页变成每次请求都渲染
export default async function Page() {
const store = await cookies()
const theme = store.get('theme')?.value
return <html data-theme={theme}>...</html>
}
这不是 bug,是设计使然------读了随请求变化的数据,当然不能再用一份缓存的 HTML 糊弄所有人。但它意味着:别在本可静态化的页面里无脑调 cookies() ,否则白白丢掉静态化的性能红利。如果只有页面的一小块需要个性化,把读 cookie 的部分抽成子组件,用 <Suspense> 包住,让页面主体仍能静态化。
坑一:在 Server Component 里 set cookie 会直接报错
cookies() 返回的对象有 .set() 和 .delete(),但你在 Server Component 里调它们会得到:
Error: Cookies can only be modified in a Server Action or Route Handler
原因是 Server Component 渲染时,HTTP 响应头可能已经开始发送了,没法再塞 Set-Cookie。读 cookie 哪都行,写 cookie 只能在 Server Action 或 Route Handler 里。 这是硬性边界。
在 Server Action 里读写 cookie:一个登录例子
Server Action 是写 cookie 的正确场所。下面是一个最小的登录流程:表单提交 → 校验 → 种 cookie → 刷新页面。
tsx
// app/login/actions.ts
'use server'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export async function login(formData: FormData) {
const password = formData.get('password')
// 这里用假校验示意,真实项目请对接你的鉴权服务
if (password !== 'secret') {
return { error: '密码错误' }
}
const cookieStore = await cookies()
cookieStore.set('token', 'signed-jwt-here', {
httpOnly: true, // JS 读不到,防 XSS 窃取
secure: true, // 只在 HTTPS 下发送
sameSite: 'lax', // 防大部分 CSRF
maxAge: 60 * 60 * 24 * 7, // 7 天
path: '/',
})
redirect('/dashboard') // 种完 cookie 跳转
}
tsx
// app/login/page.tsx
import { login } from './actions'
export default function LoginPage() {
return (
<form action={login}>
<input type="password" name="password" />
<button type="submit">登录</button>
</form>
)
}
登出就是 delete:
tsx
'use server'
import { cookies } from 'next/headers'
export async function logout() {
const store = await cookies()
store.delete('token') // 删除 cookie
}
那几个 cookie 选项别偷懒:httpOnly 挡住前端 JS 读取(防 XSS 把 token 偷走),secure 强制 HTTPS,sameSite 挡住跨站请求伪造。存登录 token 却不加 httpOnly,等于把钥匙插门上。
坑二:Route Handler 里改 cookie 要用响应对象
在 Route Handler(route.ts)里,除了用 cookies(),也可以直接操作 NextResponse,后者在需要精确控制响应时更直观:
ts
// app/api/theme/route.ts
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
const { theme } = await request.json()
const res = NextResponse.json({ ok: true })
res.cookies.set('theme', theme, { path: '/', maxAge: 31536000 })
return res // cookie 通过响应头下发
}
两种方式都行,选一种别混用。
读 header 的一个实用场景:拿真实 IP
反向代理(Nginx、Cloudflare)后面,客户端真实 IP 藏在转发头里,headers() 正好用来读:
tsx
import { headers } from 'next/headers'
async function getClientIP() {
const h = await headers()
// 优先取代理链最左边的原始 IP,退回 x-real-ip
const forwarded = h.get('x-forwarded-for')
return forwarded?.split(',')[0].trim() ?? h.get('x-real-ip') ?? 'unknown'
}
注意 x-forwarded-for 是客户端可伪造的,别拿它做安全判断(比如「只有内网 IP 能访问」),它只适合做日志、地域展示这类非安全用途。
小结
cookies()/headers()来自next/headers,只能在服务端代码里用;Next.js 15 起是异步的,记得await。- 调用它们会让路由从静态渲染切成动态渲染------别在能静态化的页面里滥用,必要时用
<Suspense>隔离个性化部分。 - 读 cookie 到处可以,写 cookie 只能在 Server Action 或 Route Handler,在 Server Component 里 set 会直接报错。
- 种登录 cookie 必带
httpOnly + secure + sameSite,否则 token 容易被偷。 x-forwarded-for可伪造,只用于日志/展示,别做安全判断。
一句话记忆:读随处、写受限;一读就动态,想静态就隔离。