Vercel 本地 Fetch POST 请求出现 CORS 预检错误
结论
浏览器已经指出了失败环节:POST 还没有发出,之前的 OPTIONS 预检请求就收到了非 2xx 响应。
在 vercel.json 中配置 CORS 响应头,只会为响应添加 Header,无法把 404、401、405、500 或重定向改成成功响应。先确认 OPTIONS /api/v1/dummy 的实际状态码,再检查该请求能否绕过鉴权、正确命中函数,并以 204 或其他 2xx 状态结束。
建议直接在 API 函数中处理完整的 CORS 流程,包括 OPTIONS 预检和实际的 POST 响应。
为什么 GET 正常,POST 却失败
页面来源为:
http://127.0.0.1:5500
API 地址为:
http://localhost:4002
协议、主机或端口只要有一项不同,浏览器就会将请求视为跨源请求。127.0.0.1 和 localhost 也属于不同的源。
以下请求头会触发 POST 的预检:
Content-Type: application/json
浏览器会先发送类似的请求:
OPTIONS /api/v1/dummy HTTP/1.1
Origin: http://127.0.0.1:5500
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
浏览器只有在 OPTIONS 返回 2xx 状态并包含匹配的 CORS 响应头后,才会继续发送 POST。
Express 示例之所以能工作,是因为 cors 中间件会自动响应预检请求。根据现有代码,还无法判断 Vercel 函数中的 ensureOptionsPreflight() 是否结束了响应,以及它返回了什么状态,因此应先检查这里。
查看 OPTIONS 的实际响应
在浏览器开发者工具的 Network 面板中筛选 dummy,打开 OPTIONS 请求并检查:
- Status Code
- Response Headers
- Request URL
- 是否发生
307或308重定向 - Vercel 本地终端是否收到该请求
- 函数日志中是否打印了
req method OPTIONS
也可以直接发送请求测试。Windows 环境建议使用 curl.exe,以免调用 PowerShell 的 curl 别名:
curl.exe -i -X OPTIONS "http://localhost:4002/api/v1/dummy" ^
-H "Origin: http://127.0.0.1:5500" ^
-H "Access-Control-Request-Method: POST" ^
-H "Access-Control-Request-Headers: content-type"
macOS 或 Linux 可以执行:
curl -i -X OPTIONS 'http://localhost:4002/api/v1/dummy' \
-H 'Origin: http://127.0.0.1:5500' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type'
正常响应应接近:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://127.0.0.1:5500
Access-Control-Allow-Methods: GET,POST,OPTIONS
Access-Control-Allow-Headers: Content-Type
Vary: Origin
可以根据状态码继续排查:
404:函数路由没有命中,或者本地开发服务加载的不是预期项目。401、403:鉴权或其他中间件拦截了预检请求。405:路由存在,但不接受OPTIONS。500:函数导入、初始化或ensureOptionsPreflight()执行失败。307、308:请求被重定向。应直接请求最终地址,并检查斜杠、重写规则和协议。- 一直没有响应:预检分支可能没有调用
res.end()、res.send()或res.json()。
在 Vercel 函数中完整处理 CORS
可以暂时移除 ensureOptionsPreflight(),改用明确的处理逻辑来验证问题:
import type { VercelRequest, VercelResponse } from '@vercel/node';
const allowedOrigins = new Set([
'http://127.0.0.1:5500',
'http://localhost:5500',
]);
export default async function handler(
req: VercelRequest,
res: VercelResponse,
) {
const origin = req.headers.origin;
if (origin && allowedOrigins.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
}
res.setHeader(
'Access-Control-Allow-Methods',
'GET, POST, OPTIONS',
);
res.setHeader(
'Access-Control-Allow-Headers',
'Content-Type, Authorization',
);
if (req.method === 'OPTIONS') {
return res.status(204).end();
}
if (origin && !allowedOrigins.has(origin)) {
return res.status(403).json({ status: 'origin not allowed' });
}
if (req.method === 'POST') {
console.log('newRes', req.body);
return res.status(200).json(req.body);
}
res.setHeader('Allow', 'POST, OPTIONS');
return res.status(405).json({ status: 'invalid method' });
}
需要留意两点:
OPTIONS必须结束响应,不能只设置 Header 后继续等待。- 实际的
POST响应也必须包含 CORS Header。即使预检成功,后续响应仍然需要Access-Control-Allow-Origin。
如果项目使用了全局鉴权、CSRF 检查或其他中间件,应在这些检查之前判断 OPTIONS。预检请求通常不带业务凭证,如果先执行鉴权,很容易返回 401 或 403。
调整 vercel.json
静态配置可以保留。建议使用完整、明确的 JSON,并避免同时使用通配符和凭证:
{
"headers": [
{
"source": "/api/(.*)",
"headers": [
{
"key": "Access-Control-Allow-Origin",
"value": "http://127.0.0.1:5500"
},
{
"key": "Access-Control-Allow-Methods",
"value": "GET,OPTIONS,PATCH,DELETE,POST,PUT"
},
{
"key": "Access-Control-Allow-Headers",
"value": "Content-Type,Authorization,X-CSRF-Token,X-Requested-With,Accept"
}
]
}
]
}
请求确实需要 Cookie 或 HTTP 认证时,再增加:
{
"key": "Access-Control-Allow-Credentials",
"value": "true"
}
前端也要明确启用凭证:
fetch(apiUrl, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(postData),
});
启用凭证后,不能使用以下组合:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Access-Control-Allow-Origin 此时必须填写具体来源,例如:
Access-Control-Allow-Origin: http://127.0.0.1:5500
如果不需要 Cookie,可以直接删除 Access-Control-Allow-Credentials。原始 fetch 示例没有设置 credentials: 'include',通常不需要允许凭证。
也不建议在 Access-Control-Allow-Headers 中把 * 与其他名称混写。当前请求只需允许 Content-Type;使用 Authorization 时,再把它明确加入。
检查 Vercel 本地环境是否加载了配置
即使函数已经正确处理 OPTIONS,也要确认本地服务是否从预期目录运行。可以进入包含 vercel.json 的项目根目录后启动:
vercel dev --listen 4002
启动后逐项确认:
vercel.json是合法、完整的 JSON。/api/v1/dummy与dummy.ts的实际目录映射一致。- 请求没有被
rewrites、redirects或框架路由改写。 - 终端日志中能看到
OPTIONS请求。 - 本机使用的 Vercel CLI 和项目依赖均已完整安装。
- 没有代理、防病毒软件或其他程序占用
4002端口。
电脑型号不会改变浏览器的 CORS 规则。如果问题是在换电脑后出现的,更可能是本地 CLI、启动目录、环境变量、依赖、路由构建结果或中间件执行路径发生了变化。
Nuxt 3 中也可以使用同源代理
如果 Nuxt 页面只访问自己的后端,可以创建 Nuxt server route。浏览器请求同源地址,再由服务端转发到 Vercel API。服务端请求不受浏览器 CORS 限制。
例如创建:
// server/api/dummy.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event);
return await $fetch('http://localhost:4002/api/v1/dummy', {
method: 'POST',
body,
});
});
页面改为请求 Nuxt 自己的地址:
const result = await $fetch('/api/dummy', {
method: 'POST',
body: {
userId: 1,
id: 101,
title: 'Custom Post Title',
body: 'Custom Post Body',
},
});
这种方式能避免浏览器发起跨源请求。不过,Vercel API 的 OPTIONS 问题仍然应该修复,因为以后可能还有其他浏览器客户端直接调用该接口。
验证修复结果
修复后按以下顺序检查:
- 单独请求
OPTIONS,确认状态为204或其他 2xx。 - 确认响应中包含与当前页面来源匹配的
Access-Control-Allow-Origin。 - 确认
Access-Control-Allow-Methods包含POST。 - 确认
Access-Control-Allow-Headers包含Content-Type。 - 从 HTML 页面或 Nuxt 客户端发送
POST。 - 如果预检成功但
POST仍被拦截,检查POST响应本身是否也包含 CORS Header。
备注:内容仅供参考。
