You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 07:32:06