From 2ce8a247079f065710d0cb2ff559f8878b52f7a8 Mon Sep 17 00:00:00 2001 From: bigcode Date: Wed, 30 Sep 2026 17:10:57 +0800 Subject: [PATCH] =?UTF-8?q?fix(deploy):=20/api/*=20=E4=B8=8E=20/internal/*?= =?UTF-8?q?=20=E6=98=BE=E5=BC=8F=20run=5Fworker=5Ffirst,=E4=BF=AE=20OAuth?= =?UTF-8?q?=20=E5=9B=9E=E8=B0=83=E8=A2=AB=20SPA=20=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E5=B1=82=E6=88=AA=E8=83=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- console/wrangler.jsonc | 9 ++++- package.json | 2 +- tests/deploy_config.test.ts | 70 +++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 tests/deploy_config.test.ts diff --git a/console/wrangler.jsonc b/console/wrangler.jsonc index 827a441..fea5c1c 100644 --- a/console/wrangler.jsonc +++ b/console/wrangler.jsonc @@ -13,7 +13,14 @@ "assets": { "directory": "./public", "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": [ // 自定义域名:wmailhub.adx9.com(CF 自动创建 DNS 记录并签发证书) diff --git a/package.json b/package.json index 96f9c27..5b2de2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wmailhub", - "version": "2.3.3", + "version": "2.3.4", "description": "WmailHub - 基于 Cloudflare Email Workers 的轻量邮件接收与转发平台(Console 直连 Email Worker)", "private": true, "type": "module", diff --git a/tests/deploy_config.test.ts b/tests/deploy_config.test.ts new file mode 100644 index 0000000..b902a2d --- /dev/null +++ b/tests/deploy_config.test.ts @@ -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 { + 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; +} + +const config = readWranglerConfig(); +const assets = config.assets as Record; +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); +});