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

NextJS 14.0.3服务端组件获取Cookies报错及API请求问题求助

问题分析与解决方案

错误根源

next/headers的cookies()是服务端专属API,仅能在Server Components、API路由、Server Actions中使用。如果在标记了"use client"的客户端组件中调用,会触发环境不匹配错误;反之,若给fetcher添加"use client",又会导致它无法在API路由(服务端环境)中被导入使用。


解决方案1:让fetcher兼容服务端/客户端双环境

通过环境判断+动态导入,避免服务端API在客户端加载,同时分别用对应方式获取token:

import { HttpStatusCode } from 'axios'
import { BASE_API_URL } from './axios'

interface FetchOptions extends RequestInit {
    queryParams?: Record<string, string>
}

function queryStringify(params: Record<string, string>): string {
    const query = new URLSearchParams(params)
    return query.toString()
}

// 抽离token获取逻辑,兼容双环境
async function getAccessToken() {
  if (typeof window === 'undefined') {
    // 服务端:动态导入next/headers,避免客户端加载
    const { cookies } = await import('next/headers')
    return cookies().get('accessToken')?.value
  } else {
    // 客户端:解析document.cookie
    const cookieMatch = document.cookie.match(/accessToken=([^;]+)/)
    return cookieMatch ? decodeURIComponent(cookieMatch[1]) : null
  }
}

export async function fetcher(url: string, options?: FetchOptions, authorizedRequest?: boolean): Promise<any> {
    try {
        const queryParams = options?.queryParams ? `?${queryStringify(options.queryParams)}` : ''
        let headers: HeadersInit = {
            ...options?.headers,
        }

        if (authorizedRequest) {
            const accessToken = await getAccessToken()

            if (accessToken) {
                headers = {
                    ...headers,
                    'Authorization': `Bearer ${accessToken}`,
                }
            } else {
                throw new Error('Access token is not available for an authorized request')
            }
        }

        if (options?.method === 'POST' || options?.method === 'PUT') {
            headers = {
                ...headers,
                'Content-Type': 'application/json',
            }
        }

        const response = await fetch(`${BASE_API_URL}${url}${queryParams}`, {
            ...options,
            headers,
        })

        if (!response.ok) {
            const status = response.status

            const errorStatusMap: Record<number, string> = {
                [HttpStatusCode.NotFound]: 'Not found',
                [HttpStatusCode.Unauthorized]: 'Unauthorized',
                [HttpStatusCode.Forbidden]: 'Forbidden',
                [HttpStatusCode.InternalServerError]: 'Internal server error',
                [HttpStatusCode.MethodNotAllowed]: 'Method not allowed',
            }

            const errorMessage = errorStatusMap[status] || response.statusText

            throw { status, message: errorMessage }
        }

        return await response.json()
    } catch (error) {
        throw error
    }
}

解决方案2:将token作为参数传入fetcher(更推荐)

移除fetcher内部的token获取逻辑,让调用方根据自身环境自行获取token并传入,彻底避免环境冲突:

修改后的fetcher

import { HttpStatusCode } from 'axios'
import { BASE_API_URL } from './axios'

interface FetchOptions extends RequestInit {
    queryParams?: Record<string, string>
}

function queryStringify(params: Record<string, string>): string {
    const query = new URLSearchParams(params)
    return query.toString()
}

export async function fetcher(url: string, options?: FetchOptions, accessToken?: string): Promise<any> {
    try {
        const queryParams = options?.queryParams ? `?${queryStringify(options.queryParams)}` : ''
        let headers: HeadersInit = {
            ...options?.headers,
        }

        if (accessToken) {
            headers = {
                ...headers,
                'Authorization': `Bearer ${accessToken}`,
            }
        }

        if (options?.method === 'POST' || options?.method === 'PUT') {
            headers = {
                ...headers,
                'Content-Type': 'application/json',
            }
        }

        const response = await fetch(`${BASE_API_URL}${url}${queryParams}`, {
            ...options,
            headers,
        })

        if (!response.ok) {
            const status = response.status

            const errorStatusMap: Record<number, string> = {
                [HttpStatusCode.NotFound]: 'Not found',
                [HttpStatusCode.Unauthorized]: 'Unauthorized',
                [HttpStatusCode.Forbidden]: 'Forbidden',
                [HttpStatusCode.InternalServerError]: 'Internal server error',
                [HttpStatusCode.MethodNotAllowed]: 'Method not allowed',
            }

            const errorMessage = errorStatusMap[status] || response.statusText

            throw { status, message: errorMessage }
        }

        return await response.json()
    } catch (error) {
        throw error
    }
}

服务端调用示例(Server Components/API路由)

import { cookies } from 'next/headers'
import { fetcher } from './fetcher'

async function fetchServerData() {
    const token = cookies().get('accessToken')?.value
    if (!token) throw new Error('No access token')
    return fetcher('/user/profile', {}, token)
}

客户端调用示例(客户端组件)

'use client'
import { fetcher } from './fetcher'

async function fetchClientData() {
    // 从document.cookie解析token
    const tokenMatch = document.cookie.match(/accessToken=([^;]+)/)
    const token = tokenMatch ? decodeURIComponent(tokenMatch[1]) : null
    if (!token) throw new Error('No access token')
    return fetcher('/user/profile', {}, token)
}

解决方案3:使用Server Actions处理授权请求(最安全)

把授权请求逻辑放在Server Action中(服务端运行),客户端只需调用该Action,无需处理token:

创建Server Action

'use server'
import { cookies } from 'next/headers'
import { fetcher } from './fetcher'

export async function fetchAuthorizedResource(url: string, options?: FetchOptions) {
    const token = cookies().get('accessToken')?.value
    if (!token) throw new Error('Access token missing')
    return fetcher(url, options, token)
}

客户端组件调用

'use client'
import { fetchAuthorizedResource } from './actions'

async function handleFetch() {
    try {
        const data = await fetchAuthorizedResource('/user/profile')
        // 处理返回的数据
    } catch (err) {
        // 处理错误
    }
}

export default function ClientComponent() {
    return <button onClick={handleFetch}>获取用户信息</button>
}

内容的提问来源于stack exchange,提问作者Кайрат Курбаналиев

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 00:57:09