Next.js生产构建报错:路由因使用Cookies无法静态渲染
问题描述
我在Next.js应用中自定义了fetch函数,用于发起请求时获取token,相关函数代码如下:
import { getNewAccessToken } from "./getNewAccessToken"; import { cookies } from "next/headers"; const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL as string; const isServer = typeof window === "undefined"; const getAuthToken = async () => { if (isServer) { const cookieStore = await cookies(); return cookieStore.get("accessToken")?.value; } else { return document.cookie .split("; ") .find((row) => row.startsWith("accessToken=")) ?.split("=")[1]; } }; export const fetchClient = async ( endpoint: string, options: RequestInit & { authRequired?: boolean } = {} ): Promise<any> => { const headers = new Headers(options.headers); if (!(options.body instanceof FormData)) { headers.set("Content-Type", "application/json"); } if (options.authRequired) { const token = await getAuthToken(); if (token) { headers.set("Authorization", `Bearer ${token}`); } } const response = await fetch(`${API_BASE_URL}/${endpoint}`, { ...options, headers, }); if (response.status === 401 && options.authRequired) { try { const newToken = await getNewAccessToken(); if (newToken) { headers.set("Authorization", `Bearer ${newToken}`); return fetchClient(endpoint, { ...options, headers }); } } catch (error) { console.error("Failed to refresh token:", error); return response.json(); } } return response.json(); };
以下是该函数的使用示例:
export const createAboutUs = async (payload: FormData) => { try { const response = await fetchClient("/about-us", { method: "POST", body: payload, authRequired: true, }); return await response; } catch (error) { console.error("Error creating about us:", error); throw new Error("Failed to create about us"); } }; export async function createAboutUsAction(formData: FormData) { try { const response = await createAboutUs(formData); return { success: true, data: response.data }; } catch (error) { console.log(error); return { error: "Failed to create About us" }; } }
表单处理逻辑如下:
const onSubmit = async (data: z.infer<typeof aboutUsFormSchema>) => { const formData = new FormData(); Object.entries(data).forEach(([key, value]) => { if (value) { if (key === "aboutUsImages" && Array.isArray(value)) { value.forEach((item) => { if (item instanceof File) { formData.append("aboutUsImages", item); } }); } else if (typeof value === "string") { formData.append(key, value); } } }); startTransition(async () => { let result; if (aboutUsData?.id) { result = await updateAboutUsAction(formData); } else { result = await createAboutUsAction(formData); } if (result.success) { toast.success( aboutUsData?.id ? "About us updated successfully" : "About us created successfully" ); router.push("/dashboard/general-setting"); router.refresh(); } else { toast.error( result.error || (aboutUsData?.id ? "Failed to update about us" : "Failed to create about us") ); } }); };
开发环境下一切运行正常,但执行生产构建时出现如下错误:
Error: Dynamic server usage: Route /dashboard/reporter couldn't be rendered statically because it used `cookies`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error I get this error for different files every time I run the build.
预期:生产构建能像开发环境一样顺利完成。
需求:希望保留当前fetchClient同时处理客户端和服务端token的逻辑,修复该构建错误。
附getNewAccessToken函数代码:
import { cookies } from "next/headers"; export const getNewAccessToken = async () => { const cookieStore = await cookies(); const refreshToken = cookieStore.get("refreshToken")?.value; try { const response = await fetch( `${process.env.NEXT_PUBLIC_API_BASE_URL}/auth/refresh-access-token`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ refreshToken }), } ); const result = await response.json(); if (result.statusCode === 200) { const accessToken = result.data.accessToken; cookieStore.set("accessToken", accessToken); } return result; } catch (error) { console.error(error); throw new Error("An error occurred while refreshing the token"); } };
解决方案
核心原因
Next.js生产构建会默认尝试静态渲染所有路由,但你的fetchClient和getNewAccessToken直接调用了服务端专属的cookies()API,这类API依赖请求上下文,无法在静态渲染过程中执行,因此触发动态服务器使用错误。
修复步骤
1. 强制动态渲染使用fetchClient的路由
在所有调用了带authRequired: true参数fetchClient的页面路由文件顶部,添加动态渲染标记,告诉Next.js该路由必须动态渲染:
// 例如 app/dashboard/reporter/page.tsx export const dynamic = "force-dynamic"; // 页面组件代码 export default function ReporterPage() { // ... 你的组件逻辑 }
2. 严格区分服务端与客户端的token刷新逻辑
getNewAccessToken中使用了服务端的cookies()修改cookie,不能在客户端执行,需要添加环境检查,并补充客户端独立的刷新逻辑:
// 修改getNewAccessToken export const getNewAccessToken = async () => { if (!isServer) { throw new Error("getNewAccessToken仅能在服务端调用"); } const cookieStore = await cookies(); const refreshToken = cookieStore.get("refreshToken")?.value; try { const response = await fetch( `${process.env.NEXT_PUBLIC_API_BASE_URL}/auth/refresh-access-token`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ refreshToken }), } ); const result = await response.json(); if (result.statusCode === 200) { const accessToken = result.data.accessToken; cookieStore.set("accessToken", accessToken); } return result; } catch (error) { console.error(error); throw new Error("An error occurred while refreshing the token"); } }; // 修改fetchClient中的401处理分支 if (response.status === 401 && options.authRequired) { try { let newToken; if (isServer) { newToken = await getNewAccessToken(); } else { // 客户端独立的token刷新逻辑 const refreshToken = document.cookie .split("; ") .find(row => row.startsWith("refreshToken=")) ?.split("=")[1]; if (refreshToken) { const clientResp = await fetch( `${API_BASE_URL}/auth/refresh-access-token`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken }), credentials: "include" } ); const result = await clientResp.json(); if (result.statusCode === 200) { newToken = result.data.accessToken; // 更新客户端cookie document.cookie = `accessToken=${newToken}; path=/; max-age=3600`; } } } if (newToken) { headers.set("Authorization", `Bearer ${newToken}`); return fetchClient(endpoint, { ...options, headers }); } } catch (error) { console.error("Failed to refresh token:", error); return response.json(); } }
3. 标记Server Action
你的createAboutUsAction属于Server Action,需要添加'use server'指令明确标记,避免静态构建时混淆:
"use server"; export async function createAboutUsAction(formData: FormData) { try { const response = await createAboutUs(formData); return { success: true, data: response.data }; } catch (error) { console.log(error); return { error: "Failed to create About us" }; } }
4. 验证修复
完成上述修改后,重新执行npm run build或pnpm build,确认动态服务器错误消失。同时测试客户端和服务端的token获取、刷新逻辑是否正常工作。
内容的提问来源于stack exchange,提问作者Saikot Roy
相关产品推荐
相关产品推荐

