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
相关产品推荐
相关产品推荐

