fix(deploy): /api/* 与 /internal/* 显式 run_worker_first,修 OAuth 回调被 SPA 资源层截胡
compatibility_date >= 2025-04-01 隐式开启 assets_navigation_prefers_asset_serving: 浏览器【导航请求】不进入 Worker,直接由 SPA 资源层返回 index.html。 /api/oauth/cloudflare/callback 是浏览器跨站重定向的落点(导航请求),于是被截胡 —— 前端在无匹配路由的路径上 hydrate,渲染 React Router 的 404 页面;后端 state 完全没被消费, 表现为「Cloudflare 授权成功,回调却毫无反应」。 用 run_worker_first 路径数组(而非 true),静态资源仍走 asset-first 快路径。 - 新增 tests/deploy_config.test.ts 锁住这条部署配置(此类问题只在生产出现) - 线上复验:导航请求打回调现在返回 Worker 的 400,不再是 200 index.html;/accounts 仍返回 SPA
This commit is contained in:
@@ -13,7 +13,14 @@
|
|||||||
"assets": {
|
"assets": {
|
||||||
"directory": "./public",
|
"directory": "./public",
|
||||||
"binding": "ASSETS",
|
"binding": "ASSETS",
|
||||||
"not_found_handling": "single-page-application"
|
"not_found_handling": "single-page-application",
|
||||||
|
// 必须显式让 /api/* 先走 Worker:compatibility_date >= 2025-04-01 会隐式开启
|
||||||
|
// assets_navigation_prefers_asset_serving,浏览器【导航请求】不进 Worker 而直接拿 index.html。
|
||||||
|
// 那样 /api/oauth/cloudflare/callback 这个浏览器重定向落点会被 SPA 资源层截胡,
|
||||||
|
// 前端在没有匹配路由的路径上 hydrate,渲染成 React Router 的 404 页面 ——
|
||||||
|
// 表现为「Cloudflare 授权明明成功,回调却毫无反应」,后端 state 都没被消费。
|
||||||
|
// 用数组(而不是 true)可让静态资源保持 asset-first 快路径,不为每个资源文件多付一次 Worker 调用。
|
||||||
|
"run_worker_first": ["/api/*", "/internal/*"]
|
||||||
},
|
},
|
||||||
"routes": [
|
"routes": [
|
||||||
// 自定义域名:wmailhub.adx9.com(CF 自动创建 DNS 记录并签发证书)
|
// 自定义域名:wmailhub.adx9.com(CF 自动创建 DNS 记录并签发证书)
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wmailhub",
|
"name": "wmailhub",
|
||||||
"version": "2.3.3",
|
"version": "2.3.4",
|
||||||
"description": "WmailHub - 基于 Cloudflare Email Workers 的轻量邮件接收与转发平台(Console 直连 Email Worker)",
|
"description": "WmailHub - 基于 Cloudflare Email Workers 的轻量邮件接收与转发平台(Console 直连 Email Worker)",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* 部署配置回归测试(console/wrangler.jsonc):
|
||||||
|
* 这类问题只在生产出现,代码与本地 bun run dev 都测不到,只能锁住配置本身。
|
||||||
|
*
|
||||||
|
* 背景(2026-09-30 线上事故):compatibility_date >= 2025-04-01 会隐式开启
|
||||||
|
* assets_navigation_prefers_asset_serving —— 浏览器【导航请求】不进入 Worker,
|
||||||
|
* 直接由 SPA 资源层返回 index.html。于是 /api/oauth/cloudflare/callback 这个
|
||||||
|
* 浏览器重定向落点被截胡,前端在无匹配路由的路径上 hydrate,渲染 React Router 的
|
||||||
|
* 404 页面;后端连 state 都没消费,表现为「Cloudflare 授权成功但毫无反应」。
|
||||||
|
* 修法是 assets.run_worker_first 显式列出 /api/*。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from "bun:test";
|
||||||
|
|
||||||
|
/** # 函数
|
||||||
|
* 读取并解析 wrangler.jsonc(去掉 // 注释与行尾注释)
|
||||||
|
*/
|
||||||
|
function readWranglerConfig(): Record<string, unknown> {
|
||||||
|
const raw = require("node:fs").readFileSync(
|
||||||
|
new URL("../console/wrangler.jsonc", import.meta.url).pathname,
|
||||||
|
"utf8",
|
||||||
|
) as string;
|
||||||
|
const stripped = raw
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => {
|
||||||
|
// 行注释:不在字符串里的 //
|
||||||
|
let out = "";
|
||||||
|
let inString = false;
|
||||||
|
let hitComment = false;
|
||||||
|
for (let i = 0; i < line.length; i++) {
|
||||||
|
const ch = line[i];
|
||||||
|
if (ch === '"') inString = !inString;
|
||||||
|
if (!inString && ch === "/" && line[i + 1] === "/") {
|
||||||
|
hitComment = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out += ch;
|
||||||
|
}
|
||||||
|
// 整行都是注释 -> 丢弃该行;值后面才跟注释 -> 去掉它带来的行尾逗号
|
||||||
|
if (out.trim() === "") return "";
|
||||||
|
return hitComment ? out.replace(/,\s*$/, "") : out;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
return JSON.parse(stripped) as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = readWranglerConfig();
|
||||||
|
const assets = config.assets as Record<string, unknown>;
|
||||||
|
const runWorkerFirst = assets?.run_worker_first as string[] | boolean | undefined;
|
||||||
|
|
||||||
|
test("assets 开启 SPA 回退时,必须显式让 /api/* 先走 Worker", () => {
|
||||||
|
expect(assets.not_found_handling).toBe("single-page-application");
|
||||||
|
// 数组形式(而不是 true):静态资源保持 asset-first 快路径
|
||||||
|
expect(Array.isArray(runWorkerFirst)).toBe(true);
|
||||||
|
const patterns = runWorkerFirst as string[];
|
||||||
|
expect(patterns).toContain("/api/*");
|
||||||
|
// 内部接口(Email Worker 两阶段接收)同理,不能被 SPA 资源层吃掉
|
||||||
|
expect(patterns).toContain("/internal/*");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("OAuth 回调路径确实落在 run_worker_first 覆盖范围内", () => {
|
||||||
|
// 与 console/src/services/cfapi.ts 的 CF_OAUTH_CALLBACK_PATH 对应:
|
||||||
|
// 它是浏览器跨站重定向回来的落点,必须由 Worker 处理而不是资源层
|
||||||
|
const patterns = runWorkerFirst as string[];
|
||||||
|
const matches = (path: string) =>
|
||||||
|
patterns.some((p) => p.endsWith("/*") && path.startsWith(p.slice(0, -1)));
|
||||||
|
expect(matches("/api/oauth/cloudflare/callback")).toBe(true);
|
||||||
|
expect(matches("/api/health")).toBe(true);
|
||||||
|
expect(matches("/internal/email/authorize")).toBe(true);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user