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

Next.js中导入依赖next/headers的组件报错问题求助

问题:Next.js App目录Server Component中API调用报错处理

错误详情

  • 触发的错误信息:

× You're importing a component that needs next/headers. That only works in a Server Component which is not supported in the pages/ directory.
错误指向serverClient.ts第1行导入{ cookies } from 'next/headers'

场景与预期

在Next.js App目录的Server Component中开发,点击按钮触发handleGenerateReport函数执行API调用,实现CSV报表生成下载。

相关代码

GenerateCSVDialog.tsx

import { Dispatch, SetStateAction } from 'react';
import { GenerateCSVDialogClient } from '@/app/[locale]/[tenant]/courses/[courseId]/learners/generateCSVDialogClient';
import { apiServerClient } from '@/lib/clients/serverClient';

export async function GenerateCSVDialog({ isOpen, setOpen, courseId }: { isOpen: boolean; courseId: string; setOpen: Dispatch<SetStateAction<boolean>>; }) {
  const handleGenerateReport = async () => {
      const res = await apiServerClient.get(`/api/fx/learners/v1/learners/${decodeURIComponent(courseId)}/`, {
        params: {
          download: "csv",
          tenant_ids: 107,
        },
      });
  };

  return (
    <GenerateCSVDialogClient
      isOpen={isOpen}
      setOpen={setOpen}
      onGenerateReport={handleGenerateReport}
    />
  );
}

serverClient.ts

import { cookies } from 'next/headers';

import axios from 'axios';

import { NEXT_LOCALE_COOKIE, defaultLocale } from '@/i18n';
import { getEdxSessionCookieName, getSession } from '@/lib/auth';
import { getCSRFToken } from '@/lib/clients/csrf/client';
import { BackendAuthError } from '@/lib/clients/errors';

export const apiServerClient = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
  withCredentials: true,
  headers: {
    'X-Mediator-Name': 'NELC Next.js Dashboard',
    'User-Agent': 'nelc-dashboard',
  },
});

apiServerClient.interceptors.request.use(async function (config) {
  // Setting request language
  config.headers['Accept-Language'] =
    cookies().get(NEXT_LOCALE_COOKIE)?.value || defaultLocale;

  // Setting session ID and tenant base.
  const { isAuthenticated, id: sessionId, tenant } = await getSession();
  const cookieName = getEdxSessionCookieName();

  if (isAuthenticated) {
    config.headers.Cookie = `${cookieName}=${sessionId}`;
    config.baseURL = tenant.lms_root_url;
  }

  if (['post', 'put', 'delete'].includes(config.method || '')) {
    const csrfToken = await getCSRFToken();
    config.headers['X-CSRFToken'] = csrfToken;
    config.headers['Referer'] = process.env.NEXT_PUBLIC_URL;

    config.headers.Cookie = `csrftoken=${csrfToken};${config.headers.Cookie}`;
  }

  return config;
});

apiServerClient.interceptors.response.use(
  (response) => response,
  (error) => {
    if (
      error.response.status === 403 &&
      process.env.REDIRECT_ON_BACKEND_NOT_AUTHENTICATED === 'true'
    ) {
      if (typeof window === 'undefined') throw new BackendAuthError(error);
      else window.location.href = '/signin?re=1';
    }
    return Promise.reject(error);
  },
);

解决方案

核心问题分析

handleGenerateReport定义在Server Component中,但作为props传递给客户端组件GenerateCSVDialogClient后,实际执行环境变为客户端。而apiServerClient依赖服务端专属的next/headers(cookies()),客户端环境无法识别该API,因此触发报错。

修复步骤

  1. 创建Server Action处理API请求
    将API调用逻辑移到Server Action中,确保所有依赖服务端API的逻辑在服务端执行:

    // app/[locale]/[tenant]/courses/[courseId]/learners/actions.ts
    'use server';
    
    import { cookies } from 'next/headers';
    import axios from 'axios';
    import { NEXT_LOCALE_COOKIE, defaultLocale } from '@/i18n';
    import { getEdxSessionCookieName, getSession } from '@/lib/auth';
    import { BackendAuthError } from '@/lib/clients/errors';
    
    export async function generateLearnerReport(courseId: string) {
      const apiClient = axios.create({
        baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
        withCredentials: true,
        headers: {
          'X-Mediator-Name': 'NELC Next.js Dashboard',
          'User-Agent': 'nelc-dashboard',
        },
      });
    
      apiClient.interceptors.request.use(async function (config) {
        // 复用原请求拦截逻辑
        config.headers['Accept-Language'] = cookies().get(NEXT_LOCALE_COOKIE)?.value || defaultLocale;
        const { isAuthenticated, id: sessionId, tenant } = await getSession();
        const cookieName = getEdxSessionCookieName();
    
        if (isAuthenticated) {
          config.headers.Cookie = `${cookieName}=${sessionId}`;
          config.baseURL = tenant.lms_root_url;
        }
    
        return config;
      });
    
      apiClient.interceptors.response.use(
        (response) => response,
        (error) => {
          if (error.response.status === 403 && process.env.REDIRECT_ON_BACKEND_NOT_AUTHENTICATED === 'true') {
            throw new BackendAuthError(error);
          }
          return Promise.reject(error);
        },
      );
    
      const res = await apiClient.get(`/api/fx/learners/v1/learners/${decodeURIComponent(courseId)}/`, {
        params: {
          download: "csv",
          tenant_ids: 107,
        },
      });
    
      return res.data;
    }
    
  2. 修改Server Component调用Server Action
    替换原handleGenerateReport为调用Server Action:

    import { Dispatch, SetStateAction } from 'react';
    import { GenerateCSVDialogClient } from '@/app/[locale]/[tenant]/courses/[courseId]/learners/generateCSVDialogClient';
    import { generateLearnerReport } from './actions';
    
    export async function GenerateCSVDialog({ isOpen, setOpen, courseId }: { isOpen: boolean; courseId: string; setOpen: Dispatch<SetStateAction<boolean>>; }) {
      return (
        <GenerateCSVDialogClient
          isOpen={isOpen}
          setOpen={setOpen}
          onGenerateReport={async () => await generateLearnerReport(courseId)}
        />
      );
    }
    
  3. 客户端组件适配Server Action调用
    确保客户端组件正确触发Server Action(Next.js会自动处理服务端调用的序列化):

    // generateCSVDialogClient.tsx
    'use client';
    
    import { Dispatch, SetStateAction } from 'react';
    import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
    
    export function GenerateCSVDialogClient({ isOpen, setOpen, onGenerateReport }: { isOpen: boolean; setOpen: Dispatch<SetStateAction<boolean>>; onGenerateReport: () => Promise<void>; }) {
      const handleConfirm = async () => {
        await onGenerateReport();
        setOpen(false);
      };
    
      return (
        <Dialog open={isOpen} onClose={() => setOpen(false)}>
          <DialogTitle>Generate CSV Report</DialogTitle>
          <DialogContent>
            <p>Do you want to generate the learner report as CSV?</p>
          </DialogContent>
          <DialogActions>
            <Button onClick={() => setOpen(false)}>Cancel</Button>
            <Button onClick={handleConfirm} variant="contained">Generate</Button>
          </DialogActions>
        </Dialog>
      );
    }
    

额外优化

  • 可将Server Action中的axios配置逻辑提取到服务端专属工具文件,避免重复代码。
  • 在客户端组件中添加错误捕获逻辑,对Server Action的执行结果做异常提示。

内容的提问来源于stack exchange,提问作者Amjad Shadid

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 14:18:11