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

Next.js App Router服务端/客户端统一请求最佳实践(含Cookie、CSRF、Header)

统一请求工具在Next.js 15 App Router中的优雅实现(处理Cookie、CSRF及请求头)

问题背景

在Next.js 15 App Router项目中,需要构建支持服务端(RSC/预取)和客户端的统一请求工具,正确处理Cookie、CSRF令牌及请求头。现有实现存在以下问题:

  • 服务端请求函数每次调用cookies()和headers(),导致串行阻塞,无法并行发起请求
  • 服务端请求与请求状态紧耦合,序列化困难
  • 代理方案需手动转发Cookie,代码冗余且破坏抽象性

核心问题解答

1. 优雅实现服务端/客户端统一请求的方案

通过环境自动识别+服务端上下文传递的方式,避免重复获取请求上下文,同时自动处理Cookie和CSRF:

步骤1:定义类型与统一接口

// types.ts
import { cookies, headers } from 'next/headers';

export type ServerContext = {
  cookies: ReturnType<typeof cookies>;
  headers: ReturnType<typeof headers>;
};

export interface FetcherOptions {
  url: string;
  method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
  body?: Record<string, any>;
  serverContext?: ServerContext; // 仅服务端调用时传入
}

步骤2:实现统一Fetcher

自动判断运行环境,服务端使用传入的上下文,客户端直接调用浏览器Fetch:

// fetcher.ts
import { clientFetch } from "./clientFetch";
import { serverFetch } from "./serverFetch";
import type { FetcherOptions } from "./types";

export const fetcher = async ({ url, method, body, serverContext }: FetcherOptions) => {
  if (typeof window === 'undefined') {
    if (!serverContext) {
      throw new Error('Server context is required for server-side requests');
    }
    return serverFetch(url, method, body, serverContext);
  } else {
    return clientFetch(url, method, body);
  }
};

步骤3:优化服务端请求函数

接收传入的上下文,避免重复调用cookies()和headers():

// serverFetch.ts
'use server';

import type { ServerContext } from "./types";

export const serverFetch = async (
  endpoint: string,
  method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE',
  body?: Record<string, any>,
  { cookies: nextCookies, headers: nextHeaders }: ServerContext
) => {
  const csrfToken = nextCookies.get('XSRF-TOKEN')?.value;

  if (!csrfToken && method !== 'GET') {
    throw new Error('CSRF token not found in server cookies');
  }

  const referer = nextHeaders.get('referer') ?? process.env.NEXT_PUBLIC_BASE_URL_FE;

  const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}${endpoint}`, {
    method,
    headers: {
      'Content-Type': 'application/json',
      Accept: 'application/json',
      Cookie: nextCookies.toString(),
      Referer: referer,
      ...(method !== 'GET' && { 'X-XSRF-TOKEN': csrfToken }),
    },
    credentials: 'include',
    body: body ? JSON.stringify(body) : undefined,
  });

  const data = await res.json();

  if (!res.ok) {
    console.error('Server request failed:', data);
    throw new Error(data.message || 'Server request failed');
  }

  return data;
};

步骤4:实现客户端请求函数

利用浏览器自动携带Cookie,从Cookie中提取CSRF令牌:

// clientFetch.ts
export const clientFetch = async (
  endpoint: string,
  method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE',
  body?: Record<string, any>
) => {
  let csrfToken: string | undefined;
  if (method !== 'GET') {
    csrfToken = document.cookie.split('; ')
      .find(row => row.startsWith('XSRF-TOKEN='))
      ?.split('=')[1];
    
    if (!csrfToken) {
      throw new Error('CSRF token not found in client cookies');
    }
  }

  const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}${endpoint}`, {
    method,
    headers: {
      'Content-Type': 'application/json',
      Accept: 'application/json',
      ...(csrfToken && { 'X-XSRF-TOKEN': csrfToken }),
    },
    credentials: 'include',
    body: body ? JSON.stringify(body) : undefined,
  });

  const data = await res.json();

  if (!res.ok) {
    console.error('Client request failed:', data);
    throw new Error(data.message || 'Client request failed');
  }

  return data;
};

服务端组件使用示例

一次获取上下文,并行发起多个请求,避免串行阻塞:

// app/page.tsx (服务端组件)
import { cookies, headers } from 'next/headers';
import { fetcher } from './fetcher';

export default async function Home() {
  const serverContext = { cookies: await cookies(), headers: await headers() };

  // 并行请求,无串行阻塞
  const [userData, postsData] = await Promise.all([
    fetcher({ url: '/api/user', method: 'GET', serverContext }),
    fetcher({ url: '/api/posts', method: 'GET', serverContext }),
  ]);

  return (
    <div>
      <h1>Welcome, {userData.name}</h1>
      <ul>
        {postsData.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

2. 服务端组件是否应始终通过/api/proxy路由请求?

不需要。直接调用后端API更高效,避免额外的网络跳转。代理方案仅适合需要统一处理请求(如全局日志、权限拦截)的场景,但会增加延迟。

若必须使用代理,服务端组件调用时需手动传递Cookie(服务端Fetch不会自动携带浏览器Cookie),示例:

// 服务端组件中调用代理
const nextCookies = await cookies();
const res = await fetch('/api/proxy', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Cookie': nextCookies.toString(),
  },
  body: JSON.stringify({ endpoint: '/api/user', method: 'GET' }),
});

但这种方式冗余且破坏抽象,不推荐服务端组件使用代理。

3. App Router架构下更优的混合请求方案

  1. 基于环境自动切换的统一Fetcher:如上述方案,服务端传递上下文,客户端自动处理Cookie和CSRF,兼顾性能与抽象性。
  2. 结合TanStack Query:在服务端组件中预取数据时传入上下文,客户端组件直接使用统一Fetcher,无需额外配置:
    // app/providers.tsx (客户端组件)
    'use client';
    
    import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
    import { fetcher } from './fetcher';
    
    const queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          fetcher: ({ queryKey }) => {
            const [url, method] = queryKey as [string, string];
            return fetcher({ url, method: method as any });
          },
        },
      },
    });
    
    export function Providers({ children }: { children: React.ReactNode }) {
      return (
        <QueryClientProvider client={queryClient}>
          {children}
        </QueryClientProvider>
      );
    }
    
  3. Laravel Sanctum适配:确保服务端请求的Referer头指向Next.js域名,客户端首次请求后端时获取XSRF-TOKEN Cookie(Laravel Sanctum会自动设置)。

内容的提问来源于stack exchange,提问作者Marko Ticinovic

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 08:42:03