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

Next.js App Router中Server Action的Fetch请求如何传递HttpOnly Cookie

场景说明

前端基于Next.js 13 App Router搭建,后端是Rails 7纯API服务,用Doorkeeper实现认证,且配置为HttpOnly Cookie方式。开发了新产品提交表单,计划通过Next.js的Server Action向后端products#create路由发送POST请求,同时在创建完成后调用revalidatePath()清除缓存,实时刷新产品列表UI。

核心问题

products#create是受保护路由,必须携带access_token Cookie才能认证,但当前遇到了无法在Server Action中正确传递该HttpOnly Cookie的问题,陷入两难选择:

  • 方案1:客户端Fetch请求:浏览器会自动携带认证Cookie,但无法触发Next.js的revalidatePath(),缓存无法清除,新产品无法实时展示。
  • 方案2:Server Action + 放弃HttpOnly:能正常调用revalidatePath()刷新缓存,但需要修改Doorkeeper配置,放弃HttpOnly Cookie的安全特性,这并非我想要的结果。

尝试过的无效方案

试过Next.js中间件,控制台打印request.headers能看到access_token,开发者工具的网络标签中也能看到该Cookie,但后端Doorkeeper始终无法获取到。而一旦移除Server Action中的revalidatePath()并删除'use server'标记(转为客户端请求),Cookie就能正常传递给后端。

疑问

这真的是两难局面吗?还是我遗漏了Next.js文档中的关键配置?我猜测Next.js服务器无法直接访问浏览器的HttpOnly Cookie,所以Server Action发起请求时无法携带,但疑惑为什么网络标签中能看到这个Cookie。希望有类似配置经验的人分享可行的最佳实践或解决方案。


Server Action 代码(actions.ts)

'use server'
export const createProduct = async (data: FormData) => {
  const url = `${ baseApiUrl() }/v1/products`;

  const response = await fetch(url, {
    credentials: 'include',
    method: 'POST',
    headers: {
      'Authorization': `Basic ${ doorkeeperCredentials() }`,
    },
    body: configureData(data)
  });

  revalidatePath('/products');

  return response.json();
};

ProductForm 组件代码(product-form.component.tsx)

'use client'

// library
import { useState, useEffect } from "react";

// api
import { createProduct } from "../../api/products-api";
import { Category } from "@/app/categories/page";
import { getAllCategories } from "@/app/api/categories-api";

const ProductForm = () => {
  // state
  const [ loading, setLoading ] = useState(true);
  const [ categories, setCategories ] = useState<Category[] | null>(null)

  useEffect(() => {
    const getCategories = async () => {
      const response = await getAllCategories();
      const categories: Category[] = await response.json();
      setCategories(categories);
    };

    categories ? setLoading(false) : getCategories();
  }, [ categories ])

  if (loading) return <p>Loading...</p>;

  return (
    <form 
      id="product"
      className="product-form"
      action={ formData => createProduct(formData) }
    >
      {/* product name */}
      <label 
        className="product-form__label"
        htmlFor="name"
      >
        Name
      </label>
      <input
        id="name" 
        className="product-form__input"
        type="text"
        autoComplete="false"
        name="product[name]"
      />

      {/* product description */}
      <label
        className="product-form__label" 
        htmlFor="short-description"
      >
        Description
      </label>
      <textarea
        id="short-description"
        className="product-form__textarea" 
        name="product[short_description]"
      />

      {/* product images */}
      <label 
        className="product-form__label"
        htmlFor="product-images"
      >
        Images
      </label>
      <input 
        id="product-images"
        className="product-form__attach-button"
        type="file"
        name="product[product_images][]"
        multiple
      />

      <div className="category-select">
        { categories && categories.map((category) => 
          <div key={ category.id }>
            <input 
              id={ `category-${ category.name }` }
              type='checkbox'
              value={ category.id }
              name={ `product[category_ids][]`}
            />
            <label htmlFor={  `category-${ category.name }` }>
              { category.name }
            </label>
          </div>
        )}
      </div>

      {/* submit button */}
      <button 
        className="product-form__button"
        type='submit'
      >
        Submit
      </button>
    </form>
  )
};

export default ProductForm;

nextConfig 配置(next.config.js)

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    serverActions: true
  },
  images: {
    domains: [
      'localhost'
    ]
  }
}

module.exports = nextConfig

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 19:45:09