这篇文章记录一次真实的线上排查过程:一个 Next.js 热榜聚合项目,本地运行正常,部署到 Cloudflare Workers 后,哔哩哔哩、虎扑、快手、懂车帝等来源加载失败。
这次案例来自我自己维护的热榜聚合站:soso.re。它聚合了微博、知乎、哔哩哔哩、虎扑、快手、V2EX 等多个来源,线上环境部署在 Cloudflare Workers 上。如果你想先看最终效果,可以直接打开 https://soso.re 体验。

最终采用的方案是:
- Cloudflare Workers 继续负责前端和大部分 API。
- 对容易被限制的热榜源,走一台自己的轻量服务器做固定路径代理。
- 对已经需要登录验证、页面结构不稳定的源,不继续硬绕,直接换成更稳定的公开源。
本文的配置示例以域名 example.com 代替真实域名,密钥也全部用占位符,请不要直接复制弱密钥到生产环境。
问题现象
项目部署到 Cloudflare Workers 后,部分 API 在线上返回空数据或错误,但本地开发环境正常。
典型表现:
/api/bilibili 失败
/api/hupu 失败
/api/kuaishou 失败
/api/dongchedi 解析为空或需要登录验证
本地请求正常,说明代码解析逻辑未必有问题;线上失败,更多是运行环境、出口 IP、请求头、平台反爬策略导致。
第一步:先加调试日志,不要盲改
最开始不要直接换接口或乱加请求头,先让 Worker 把上游响应状态、内容类型和响应体开头打印出来。
示例调试函数:
const HOT_SOURCE_DEBUG_PREFIX = '[hot-source-debug]';
const BODY_PREVIEW_LENGTH = 300;
export async function readHotSourceText(
source: string,
upstreamUrl: string,
response: Response,
) {
const body = await response.text();
console.error(HOT_SOURCE_DEBUG_PREFIX, {
source,
upstreamUrl,
status: response.status,
ok: response.ok,
contentType: response.headers.get('content-type'),
bodyLength: body.length,
bodyStart: body.slice(0, BODY_PREVIEW_LENGTH),
});
return body;
}
然后在 Cloudflare Dashboard 里打开:
Workers 和 Pages -> 你的 Worker -> Observability -> Workers Logs
可以按 [hot-source-debug] 搜索日志。
实际排查结果
线上日志里看到的情况大概是这样:
| 来源 | Worker 直连结果 | 说明 |
|---|---|---|
| 哔哩哔哩 | 412 text/html | 上游拒绝了 Cloudflare Workers 的请求特征 |
| 虎扑 | 405 text/html | 页面请求在 Workers 环境下被拒绝 |
| 快手 | 525 或页面结构缺失 | TLS/反爬/页面结构都可能影响 |
| 懂车帝 | 200 text/html 但解析为空 | 页面进入登录或验证态,原解析字段不存在 |
结论:这不是简单的网络波动,而是部分平台对 Cloudflare Workers 的出口、请求头或请求环境有限制。
方案选择
我最终没有选择把所有数据源都换成第三方聚合 API,因为这样会引入新的不确定性:第三方接口可能限流、变更、失效,甚至无法确认数据来源。
最终方案:
- 哔哩哔哩、虎扑、快手:走自己的服务器做固定路径代理。
- 懂车帝:由于页面需要验证登录,不继续绕,替换成 V2EX 官方热门主题接口。
- Worker 代码里保留原始上游地址作为 fallback,没有代理变量时仍可本地调试。
整体架构
以 soso.re 为例,用户访问的是 Cloudflare Workers 上的站点;大部分热榜源由 Worker 直接请求,少数容易被限制的来源再转到自己的服务器代理。
浏览器
|
v
Cloudflare Workers / Pages
|
| 大部分热榜源直接 fetch
|
| 对特殊来源:
v
自有服务器 Nginx / OpenResty
|
v
Bilibili / Hupu / Kuaishou 上游
代理服务器只开放固定路径,例如:
/hot/bilibili
/hot/hupu
/hot/kuaishou
不要做成通用代理,也不要把目标 URL 作为参数暴露出去。
Worker 侧代码
项目里增加一个代理选择函数:
type ProxySource = 'bilibili' | 'hupu' | 'kuaishou';
const proxyPaths: Record<ProxySource, string> = {
bilibili: '/hot/bilibili',
hupu: '/hot/hupu',
kuaishou: '/hot/kuaishou',
};
export function getHotProxyRequest(
source: ProxySource,
fallbackUrl: string,
init?: RequestInit,
): { url: string; init?: RequestInit; proxied: boolean } {
const proxyBase = process.env.HOT_PROXY_BASE?.replace(/\/$/, '');
const proxyToken = process.env.HOT_PROXY_TOKEN;
if (!proxyBase || !proxyToken) {
return { url: fallbackUrl, init, proxied: false };
}
const headers = new Headers(init?.headers);
headers.set('x-proxy-token', proxyToken);
return {
url: `${proxyBase}${proxyPaths[source]}`,
init: {
...init,
headers,
},
proxied: true,
};
}
业务接口里使用:
const upstreamUrl = 'https://api.bilibili.com/x/web-interface/ranking/v2?rid=0&type=all&callback=__jp0';
const request = getHotProxyRequest('bilibili', upstreamUrl, {
headers: {
Referer: 'https://www.bilibili.com/ranking/all',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36',
},
});
const response = await fetch(request.url, request.init);
const text = await readHotSourceText('bilibili', request.url, response);
这样有两个好处:
- 本地没有配置代理变量时,仍然走原始上游地址。
- 线上配置了代理变量后,只影响指定数据源。
Cloudflare 变量配置
在 Cloudflare 后台添加运行时变量:
HOT_PROXY_BASE=https://example.com
HOT_PROXY_TOKEN=一段足够长的随机密钥
注意入口:
Workers 和 Pages -> 项目 -> 设置 -> 变量和密钥
这里要选 变量和密钥,不是 构建变量和密钥。
原因是:
HOT_PROXY_BASE和HOT_PROXY_TOKEN是运行时请求 API 时使用的。- 构建变量只在 build 阶段可用,不能保证 Worker 运行时拿得到。
如果你使用 GitHub Actions、Wrangler 或 OpenNext 自动部署,还要注意:部署配置可能覆盖 Dashboard 里手动添加的变量。部署命令需要保留 Dashboard 变量,例如 OpenNext/Wrangler 场景中使用类似 --keep-vars 的参数。
Nginx / OpenResty 代理配置
下面配置放在原网站的同一个 server 块里,并且要放在普通 location / 之前。
先准备一个强随机密钥:
openssl rand -hex 32
假设得到:
REPLACE_WITH_LONG_RANDOM_TOKEN
哔哩哔哩代理
location = /hot/bilibili {
if ($http_x_proxy_token != "REPLACE_WITH_LONG_RANDOM_TOKEN") {
return 403;
}
proxy_pass_request_headers off;
proxy_ssl_server_name on;
proxy_ssl_name api.bilibili.com;
proxy_set_header Host api.bilibili.com;
proxy_set_header User-Agent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";
proxy_set_header Accept "application/json, text/plain, */*";
proxy_set_header Accept-Language "zh-CN,zh;q=0.9,en;q=0.8";
proxy_set_header Referer "https://www.bilibili.com/";
proxy_set_header Connection "";
proxy_pass https://api.bilibili.com/x/web-interface/ranking/v2?rid=0&type=all&callback=__jp0;
}
这里最关键的是:
proxy_pass_request_headers off;
它的作用是不要把 Cloudflare Worker 请求 example.com 时带来的原始请求头继续透传给 B 站。否则上游仍然能看到一些异常请求特征,可能继续返回 412。
虎扑代理
location = /hot/hupu {
if ($http_x_proxy_token != "REPLACE_WITH_LONG_RANDOM_TOKEN") {
return 403;
}
proxy_pass_request_headers off;
proxy_ssl_server_name on;
proxy_ssl_name bbs.hupu.com;
proxy_set_header Host bbs.hupu.com;
proxy_set_header User-Agent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";
proxy_set_header Accept "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
proxy_set_header Accept-Language "zh-CN,zh;q=0.9,en;q=0.8";
proxy_set_header Referer "https://bbs.hupu.com/";
proxy_set_header Connection "";
proxy_pass https://bbs.hupu.com/all-gambia;
}
快手代理
location = /hot/kuaishou {
if ($http_x_proxy_token != "REPLACE_WITH_LONG_RANDOM_TOKEN") {
return 403;
}
proxy_pass_request_headers off;
proxy_ssl_server_name on;
proxy_ssl_name www.kuaishou.com;
proxy_set_header Host www.kuaishou.com;
proxy_set_header User-Agent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";
proxy_set_header Accept "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
proxy_set_header Accept-Language "zh-CN,zh;q=0.9,en;q=0.8";
proxy_set_header Referer "https://www.kuaishou.com/";
proxy_set_header Connection "";
proxy_pass https://www.kuaishou.com/?isHome=1;
}
原网站 location 不要被影响
如果这台服务器原本还跑着网站后台,例如 Halo、WordPress、管理面板等,原来的 location / 一定不要加 proxy_pass_request_headers off。
推荐保持类似这样:
location / {
proxy_pass_request_headers on;
proxy_set_header Host $host;
proxy_set_header Cookie $http_cookie;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_pass http://127.0.0.1:8090;
}
修改完成后检查并重载:
nginx -t
systemctl reload nginx
测试代理是否正常
不带密钥应该返回 403:
curl -I https://example.com/hot/bilibili
带密钥应该返回 200:
curl -I \
-H "x-proxy-token: REPLACE_WITH_LONG_RANDOM_TOKEN" \
https://example.com/hot/bilibili
再分别测试:
curl -I -H "x-proxy-token: REPLACE_WITH_LONG_RANDOM_TOKEN" https://example.com/hot/hupu
curl -I -H "x-proxy-token: REPLACE_WITH_LONG_RANDOM_TOKEN" https://example.com/hot/kuaishou
如果本机 curl 是 200,但 Worker 日志里仍然是上游错误,就继续看 Worker 调用代理后的日志。
Worker 日志如何判断问题
看 [hot-source-debug] 里的 upstreamUrl。
如果还是原始地址:
https://api.bilibili.com/...
说明 Worker 没有读到代理变量,重点检查 Cloudflare 的运行时变量。
如果已经变成代理地址:
https://example.com/hot/bilibili
说明变量生效了,问题在代理服务器到上游这一段。
常见状态码判断:
| 状态 | 可能原因 | 处理方式 |
|---|---|---|
403 | 代理密钥不匹配,或请求头没带上 | 检查 HOT_PROXY_TOKEN 和 Nginx 里的密钥是否完全一致 |
404 | Nginx 没匹配到 /hot/* location | 检查配置是否放在正确的 server 块里 |
412 | 上游识别到异常请求特征 | 检查 /hot/* 是否设置了 proxy_pass_request_headers off 和浏览器请求头 |
405 | 上游拒绝当前请求方式或环境 | 使用固定路径代理,并伪装正常浏览器请求头 |
525 | SSL 握手或上游 TLS 问题 | 检查 proxy_ssl_server_name on 和 proxy_ssl_name |
200 但解析为空 | 页面结构变了,或进入登录/验证页面 | 打印 bodyStart,必要时换源 |
懂车帝为什么换成 V2EX
懂车帝这类页面如果进入登录验证态,HTML 仍然可能返回 200,但页面里的业务数据已经没有了。
这时继续堆请求头、Cookie、解析规则,维护成本会很高,而且容易反复失效。
我的处理方式是换成 V2EX 热门主题接口:
https://www.v2ex.com/api/topics/hot.json
接口返回 JSON,结构清晰,不需要解析 HTML,比继续绕验证页稳定。
GitHub Actions 会不会影响仓库代码
单纯用 GitHub Actions 部署,不会自动修改仓库代码。
它通常只是:
拉取代码 -> 安装依赖 -> 构建 -> 部署到 Cloudflare
只有你的 workflow 里明确执行了 git commit、git push 这类命令,才会改仓库。
真正需要注意的是变量:
- GitHub Actions 里的 Secrets 是给 CI 流程用的。
- Cloudflare Dashboard 的 Variables and Secrets 是给 Worker 运行时用的。
- 如果部署工具会同步 Wrangler 配置,可能覆盖 Dashboard 里手动设置的变量,所以要使用保留变量的部署参数,或把变量也纳入部署配置管理。
坑点总结
1. 本地正常不代表 Workers 正常
Cloudflare Workers 的出口、TLS、请求头和普通本机环境不一样。部分站点会对这类请求环境做限制。
2. 先看上游响应,不要只看前端失败
如果只看前端页面,会误以为是组件或缓存问题。实际要看 Worker 日志里的:
status
contentType
bodyStart
upstreamUrl
3. proxy_pass_request_headers off 只能用于热榜代理
这个配置不能放在 server 级别,也不能放进原网站的 location /。
否则会导致原网站登录后台拿不到 Cookie、CSRF 等请求信息,表现为:
点击登录后直接 Access Denied
4. /cdn-cgi/rum 返回 204 是正常的
这是 Cloudflare 的 Web Analytics / RUM 上报接口。
204 No Content 表示已接收但不返回内容,不是登录失败的原因。
5. Cloudflare Rocket Loader 可能影响后台登录页
如果你的网站后台登录页依赖前端 JS 加密密码或处理表单,Cloudflare 的 Rocket Loader、Auto Minify JS 等优化可能改写脚本执行时机。
遇到登录异常时,可以对后台路径关闭这些优化,例如:
/login*
/admin*
/console*
6. 代理密钥不要用弱口令
不要用 123456、password 这类简单密钥。生成长随机值:
openssl rand -hex 32
如果密钥曾经在聊天记录、日志、截图或仓库里出现过,要立即更换。
7. 不要做开放代理
只开放固定路径:
/hot/bilibili
/hot/hupu
/hot/kuaishou
不要提供:
/proxy?url=https://...
开放代理很容易被滥用,也会给服务器带来安全和流量风险。
最终检查清单
上线前可以按这个顺序检查:
- Nginx
nginx -t通过。 - 不带
x-proxy-token访问/hot/*返回403。 - 带
x-proxy-token访问/hot/*返回200。 - Cloudflare 运行时变量里配置了
HOT_PROXY_BASE。 - Cloudflare 运行时变量里配置了
HOT_PROXY_TOKEN。 - Worker 日志里的
upstreamUrl已变成代理地址。 -
/api/bilibili、/api/hupu、/api/kuaishou返回正常数据。 - 原网站后台登录仍然正常。
- 公开过的代理密钥已经轮换。
结论
这类问题的核心不是“Cloudflare Workers 不能用”,而是热榜项目依赖了大量第三方站点页面和接口。第三方站点对请求环境非常敏感,本地正常、线上失败很常见。
比较稳的处理方式是:
- 能直连的源继续直连。
- 容易被限制但仍然能稳定获取的源,走自有服务器固定路径代理。
- 需要登录验证或结构频繁变化的源,及时换成更稳定的数据源。
- 保留足够的调试日志,用状态码和响应体片段定位问题,而不是猜。
这样整体维护成本会低很多,也不会因为一个热榜源失效拖垮整个项目。
我已经把这套方案用在 soso.re 上。如果你也在做热榜聚合、导航页、信息流聚合这类项目,可以参考上面的代理方式;如果只是想直接看各平台热点,也欢迎把 soso.re 当作日常入口。
【soso.re】聚合热榜 | CloudflareWorkers部分接口失效的排查与代理方案
https://java.li/archives/soso_re
评论