本文简介
本文将介绍一种通过 Cloudflare 的 workers 功能进行代理请求的方法。
- 用途:对那些我们无法控制服务端,又不得不通过前端调用,还不支持跨域请求的 API 接口,我们只能通过服务器代理转发。这时候就可以使用 Cloudflare 的 workers 进行代理。
- 优点:Cloudflare 的 workers 功能开通免费,每日有 10 万次免费调用额度,免费提供域名(可以绑定自己的域名)。
- 缺点:Cloudflare 的 workers 请求需要使用魔法上网。
- 我的用途:我使用 coze(扣子)编排的工作流想通过前端直接调用,但 coze(扣子)的工作流不支持跨域。我的网站部署在 Github Pages 中,我又不想买服务器,所以采取了这种办法。
- 备注:以下示例都是围绕 coze(扣子)工作流转发/代理进行介绍。
准备工作
- 注册 cloudflare 账户, cloudflare 支持邮箱、谷歌账号、苹果账号以及 Github 账号注册和登录。备注:此处不需要任何资质、备案信息
- 创建 workers,路径:计算和 AI -> Workers 和 Pages -> 创建应用程序 -> 从 Hello World! 开始
- 打开上一步创建的 workers,在右上角选择 编辑代码
监听/拦截请求
Cloudflare 的 workers 请求有统一的模版,当 workers 监听到请求,会自行调用 fetch 函数,只需在 fetch 中编写逻辑代码即可,示例如下。
- 参数介绍:
- request:Fetch API 的 Request 对象。包含 Http 的所有请求信息,如:URL、请求方法、请求头、请求体等
- env:绑定到当前 workers 中的环境变量,如 coze 的鉴权 token、请求 host 都可以绑定到环境变量中,使用 env 进行访问
- 绑定路径:打开相应的 workers -> 顶部设置按钮 -> 找到变量和机密模块,添加时名称为变量名称
- ctx: 执行上下文,此处本文为使用,请需要的朋友自行查询。
- handleRequest:具体的请求处理逻辑
export default {
async fetch(request, env, ctx) {
return handleRequest(request, env);
}
};
代理转发
此处介绍转发/代理的具体逻辑,也就是 handleRequest 函数。
实现逻辑:
- cors 返回头设置允许跨域 公有变量,用于复用信息
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, token',
};
- 判断请求类型是否为 OPTIONS,对所有 OPTIONS 请求返回 204 并在返回头中设置 cors 允许跨域。备注:OPTIONS 请求为浏览器跨域预检请求。
// 处理预检请求(OPTIONS)
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: {
...corsHeaders,
'Access-Control-Max-Age': '86400'
}
});
}
- 由于 coze 的工作流请求为 POST 类型,所以对非 POST 请求返回 405,有不同需求的朋友请自行根据需求编写。
// 只允许 POST 请求(Coze 工作流使用 POST)
if (request.method !== 'POST') {
return new Response(JSON.stringify({ error: '请求方式不支持,仅支持 POST' }), {
status: 405,
headers: {
'Content-Type': 'application/json',
...corsHeaders
}
});
}
- 从 env 获取 coze host 以及 token,如果不存在则返回 500,并提示凭证错误
// Coze API 配置
const COZE_BASE_URL = env.COZE_BASE_URL;
const COZE_TOKEN = env.COZE_TOKEN;
// 检查环境变量
if (!COZE_BASE_URL || !COZE_TOKEN) {
return new Response(JSON.stringify({
error: '凭证错误',
message: 'COZE_BASE_URL/COZE_TOKEN 环境变量不存在'
}), {
status: 500,
headers: {
'Content-Type': 'application/json',
...corsHeaders
}
});
}
- 从 request 中解析出 URL 路径、query 参数,与 coze 工作流域名拼接为完整的 URL。备注:此处也可以对 URL 设置白名单过滤,只放行白名单中的 URL,示例中未开启。
// 解析请求 URL,获取路径和查询参数
const url = new URL(request.url);
const path = url.pathname; // 例如: /v1/workflow/run
const searchParams = url.search;
// 安全验证:只允许特定的 Coze API 路径(防止滥用)
const allowedPaths = [
'/v1/workflow/run',
'/v1/bot/chat',
'/v3/chat',
'/v1/conversation/create',
'/v1/conversation/message/create'
];
// 如果路径不在白名单中,返回错误(可选)
/*
if (!allowedPaths.some(allowed => path.startsWith(allowed))) {
return new Response(JSON.stringify({
error: 'Invalid Path',
message: `Path ${path} is not allowed. Allowed paths: ${allowedPaths.join(', ')}`
}), {
status: 403,
headers: {
'Content-Type': 'application/json',
...corsHeaders
}
});
}
*/
// 构建完整的 Coze API URL
const apiUrl = `${COZE_BASE_URL}${path}${searchParams}`;
console.log(`Proxying request to: ${apiUrl}`); // 在 Cloudflare Logs 中查看
- 从 request 中解析出 headers、body。
// 构建请求头
const headers = new Headers();
headers.set('Authorization', `Bearer ${COZE_TOKEN}`);
// 保留客户端的关键头部,如果请求头存在 Authorization 则使用请求头中的 Authorization
for (const [key, value] of request.headers.entries()) {
const lowerKey = key.toLowerCase();
if (['Authorization', 'content-type', 'accept', 'user-agent', 'token'].includes(lowerKey)) {
headers.set(key, value);
}
}
// 获取请求体
const requestBody = await request.text();
- 请求 coze 工作流,将工作流返回信息包装为 Response 对象进行返回。备注:此处返回时返回头需要和预检请求 OPTIONS 一样设置 CORS 允许跨域。
// 转发请求
const response = await fetch(apiUrl, {
method: 'POST',
headers: headers,
body: requestBody
});
// 返回响应
const responseBody = await response.text();
return new Response(responseBody, {
status: response.status,
statusText: response.statusText,
headers: {
'Content-Type': response.headers.get('Content-Type') || 'application/json',
...corsHeaders
}
});
完整源码
export default {
async fetch(request, env, ctx) {
return handleRequest(request, env);
}
};
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, token',
};
async function handleRequest(request, env) {
// 处理预检请求(OPTIONS)
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: {
...corsHeaders,
'Access-Control-Max-Age': '86400'
}
});
}
// 只允许 POST 请求(Coze 工作流使用 POST)
if (request.method !== 'POST') {
return new Response(JSON.stringify({ error: '请求方式不支持,仅支持 POST' }), {
status: 405,
headers: {
'Content-Type': 'application/json',
...corsHeaders
}
});
}
// Coze API 配置
const COZE_BASE_URL = env.COZE_BASE_URL;
const COZE_TOKEN = env.COZE_TOKEN;
// 检查环境变量
if (!COZE_BASE_URL || !COZE_TOKEN) {
return new Response(JSON.stringify({
error: '凭证错误',
message: 'COZE_BASE_URL/COZE_TOKEN 环境变量不存在'
}), {
status: 500,
headers: {
'Content-Type': 'application/json',
...corsHeaders
}
});
}
try {
// 解析请求 URL,获取路径和查询参数
const url = new URL(request.url);
const path = url.pathname; // 例如: /v1/workflow/run
const searchParams = url.search;
// 安全验证:只允许特定的 Coze API 路径(防止滥用)
const allowedPaths = [
'/v1/workflow/run',
'/v1/bot/chat',
'/v3/chat',
'/v1/conversation/create',
'/v1/conversation/message/create'
];
// 如果路径不在白名单中,返回错误(可选)
/*
if (!allowedPaths.some(allowed => path.startsWith(allowed))) {
return new Response(JSON.stringify({
error: 'Invalid Path',
message: `Path ${path} is not allowed. Allowed paths: ${allowedPaths.join(', ')}`
}), {
status: 403,
headers: {
'Content-Type': 'application/json',
...corsHeaders
}
});
}
*/
// 构建完整的 Coze API URL
const apiUrl = `${COZE_BASE_URL}${path}${searchParams}`;
console.log(`Proxying request to: ${apiUrl}`); // 在 Cloudflare Logs 中查看
// 构建请求头
const headers = new Headers();
headers.set('Authorization', `Bearer ${COZE_TOKEN}`);
// 保留客户端的关键头部,如果请求头存在 Authorization 则使用请求头中的 Authorization
for (const [key, value] of request.headers.entries()) {
const lowerKey = key.toLowerCase();
if (['Authorization', 'content-type', 'accept', 'user-agent', 'token'].includes(lowerKey)) {
headers.set(key, value);
}
}
// 获取请求体
const requestBody = await request.text();
// 转发请求
const response = await fetch(apiUrl, {
method: 'POST',
headers: headers,
body: requestBody
});
// 返回响应
const responseBody = await response.text();
return new Response(responseBody, {
status: response.status,
statusText: response.statusText,
headers: {
'Content-Type': response.headers.get('Content-Type') || 'application/json',
...corsHeaders
}
});
} catch (error) {
console.error('Proxy error:', error);
return new Response(JSON.stringify({
error: 'Proxy error',
message: error.message
}), {
status: 500,
headers: {
'Content-Type': 'application/json',
...corsHeaders
}
});
}
}