1. 项目概述
最近在开发一个基于Astro框架的静态网站时,遇到了一个令人头疼的问题:表单提交总是失败,控制台报错"fetch failed"。经过仔细排查,发现问题出在Cloudflare Workers的CORS跨域处理上。这篇文章将详细记录整个排查过程和最终解决方案。
作为一名全栈开发者,我经常需要在前后端分离的架构中处理跨域问题。这次的问题特别典型,因为涉及到了Cloudflare Workers这种无服务器环境,与传统的后端服务有些不同。通过这次经历,我总结出了一套在Cloudflare Workers中处理CORS问题的完整方案。
2. 问题现象与初步分析
2.1 错误日志解读
在开发过程中,控制台出现了两种不同的错误信息:
第一种是关于CSP(内容安全策略)的警告:
code复制Executing inline script violates the following Content Security Policy...
chrome-extension://a05f635f...
这个错误实际上是由浏览器扩展(插件)注入脚本被CSP拦截导致的。关键点在于:
- 错误来源是
chrome-extension://...前缀 - 与我们的Astro项目、Worker或表单完全无关
- 不会影响实际的接口请求
第二种才是真正导致表单提交失败的CORS错误:
code复制Access to fetch at 'https://xxx.workers.dev/api/contact'
from origin 'http://localhost:4321'
has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header
这个错误包含了几个关键信息:
- 浏览器向Worker发起了OPTIONS预检请求
- Worker没有返回必要的CORS头信息
- 浏览器直接拦截了请求,导致ERR_FAILED错误
2.2 为什么会出现OPTIONS预检请求
OPTIONS预检请求是浏览器在发送某些类型的跨域请求前自动发起的检查。在我的案例中,触发了预检请求的条件包括:
- 使用了
Content-Type: application/json头 - 请求方法是POST
- 跨域访问(从localhost:4321到workers.dev域名)
当这三个条件同时满足时,浏览器一定会先发送OPTIONS请求来检查服务器是否允许跨域请求。
3. CORS问题深度解析
3.1 CORS工作机制详解
CORS(跨源资源共享)是一种安全机制,它允许网页从不同的域请求受限资源。其工作流程大致如下:
- 浏览器检测到跨域请求时,会先发送OPTIONS预检请求
- 服务器需要响应适当的CORS头
- 浏览器检查响应头,决定是否允许实际请求继续
在Cloudflare Workers环境中,我们需要手动实现这个响应逻辑,因为默认情况下Worker不会自动添加这些头。
3.2 当前Worker的问题所在
检查我的Worker代码后发现,它缺少了对OPTIONS方法的处理逻辑:
javascript复制if (request.method === 'OPTIONS') {
// 这里应该处理预检请求
}
由于没有这个处理逻辑,当浏览器发送OPTIONS请求时,Worker要么返回404,要么返回不带CORS头的响应,导致浏览器拦截后续请求。
4. 完整解决方案
4.1 生产可用的Worker模板
经过多次调试,我整理出了一份可直接用于生产的Worker代码模板:
javascript复制const corsHeaders = {
'Access-Control-Allow-Origin': 'http://localhost:4321', // 本地开发环境
// 生产环境可以换成你的实际域名
// 'https://yourdomain.com'
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
}
export default {
async fetch(request, env) {
// 1️⃣ 处理CORS预检请求
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: corsHeaders,
})
}
// 2️⃣ 只允许POST方法
if (request.method !== 'POST') {
return new Response('Method Not Allowed', {
status: 405,
headers: corsHeaders,
})
}
// 3️⃣ 解析请求数据
const { name, email, phone, message } = await request.json()
if (!name || !email || !message) {
return new Response(
JSON.stringify({ error: 'Missing fields' }),
{
status: 400,
headers: {
...corsHeaders,
'Content-Type': 'application/json',
},
}
)
}
// 4️⃣ 调用Resend发送邮件
const res = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'Website Contact <no-reply@YOUR_VERIFIED_DOMAIN.com>',
to: ['your@email.com'],
subject: 'New Contact Message',
html: `
<p><b>Name:</b> ${name}</p>
<p><b>Email:</b> ${email}</p>
<p><b>Phone:</b> ${phone || '-'}</p>
<p><b>Message:</b> ${message}</p>
`,
}),
})
if (!res.ok) {
const err = await res.text()
return new Response(err, {
status: 500,
headers: corsHeaders,
})
}
// 5️⃣ 成功响应
return new Response(
JSON.stringify({ success: true }),
{
headers: {
...corsHeaders,
'Content-Type': 'application/json',
},
}
)
},
}
4.2 前端代码说明
前端使用的是标准的fetch API,代码本身没有问题:
javascript复制await fetch('https://icy-truth-83e1.bennyxqg.workers.dev/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
})
正如注释所说,CORS是后端的责任,前端只需要确保发送正确的请求即可。
5. 进阶优化方案
5.1 允许多个来源
在实际项目中,我们通常需要同时支持开发环境(localhost)和生产环境域名。可以通过以下方式实现:
javascript复制const origin = request.headers.get('Origin')
const allowed = ['http://localhost:4321', 'https://yourdomain.com']
if (allowed.includes(origin)) {
corsHeaders['Access-Control-Allow-Origin'] = origin
}
这种方法比硬编码单个来源更灵活,可以同时支持多个环境。
5.2 临时调试方案(不推荐生产环境使用)
在开发调试阶段,可以使用通配符允许所有来源:
javascript复制'Access-Control-Allow-Origin': '*'
但要注意,这种方式在生产环境中存在安全隐患,只建议在开发阶段临时使用。
6. 常见问题与排查技巧
6.1 问题排查流程
当遇到CORS问题时,建议按照以下步骤排查:
- 检查浏览器控制台的完整错误信息
- 使用开发者工具的Network面板查看请求/响应详情
- 确认OPTIONS请求是否被正确处理
- 检查响应头是否包含必要的CORS头
6.2 常见错误与解决方案
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| 没有OPTIONS请求 | 请求不符合预检条件 | 检查Content-Type和方法 |
| OPTIONS请求返回404 | Worker未处理OPTIONS方法 | 添加OPTIONS方法处理 |
| 缺少CORS头 | Worker未设置响应头 | 确保所有响应都包含CORS头 |
| 凭证模式不匹配 | 使用了credentials: 'include' | 设置Access-Control-Allow-Credentials: true |
6.3 性能优化建议
- 对于频繁的OPTIONS请求,可以设置
Access-Control-Max-Age头来缓存预检结果 - 尽量减少需要预检的请求类型
- 在生产环境中避免使用通配符(*)
7. 实际应用中的经验分享
在实际项目中使用这套方案后,我总结出几点重要经验:
-
环境区分很重要:开发和生产环境使用不同的允许来源列表,避免开发时的配置影响生产环境。
-
头信息要完整:不仅需要设置
Access-Control-Allow-Origin,还需要根据实际情况设置Access-Control-Allow-Methods和Access-Control-Allow-Headers。 -
错误处理要全面:所有可能的响应路径(包括错误响应)都需要包含CORS头,否则错误情况下的响应也会被浏览器拦截。
-
测试要全面:不仅要测试成功的情况,还要测试各种错误场景(如缺少字段、服务器错误等)下的CORS行为。
-
监控不能少:在生产环境中,应该监控OPTIONS请求的比例和失败情况,这可以帮助发现潜在的CORS问题。