No description
  • TypeScript 90.7%
  • Shell 9.3%
Find a file
2026-08-08 06:00:44 +00:00
src Refactor to Cloudflare Workers (#1) 2026-08-08 06:00:44 +00:00
test Refactor to Cloudflare Workers (#1) 2026-08-08 06:00:44 +00:00
.gitattributes init 2026-02-06 13:30:16 +08:00
.gitignore Refactor to Cloudflare Workers (#1) 2026-08-08 06:00:44 +00:00
compare.sh Refactor to Cloudflare Workers (#1) 2026-08-08 06:00:44 +00:00
LICENSE init 2026-02-06 13:30:16 +08:00
package-lock.json Refactor to Cloudflare Workers (#1) 2026-08-08 06:00:44 +00:00
package.json Refactor to Cloudflare Workers (#1) 2026-08-08 06:00:44 +00:00
README.md Refactor to Cloudflare Workers (#1) 2026-08-08 06:00:44 +00:00
tsconfig.json Refactor to Cloudflare Workers (#1) 2026-08-08 06:00:44 +00:00
wrangler.toml Refactor to Cloudflare Workers (#1) 2026-08-08 06:00:44 +00:00

mo4-version-api · Cloudflare Workers 版

src/.NET 10 / ASP.NET Core那份搬到 Workers + KV。 对外行为除三条状态码外逐条照搬——那三条是 08-08 维护者修掉的原实现怪异处,见下表。

线上现址:https://mov.srcz.one/api/v1/version


为什么只改了存储

原实现把整个 Versions 文档写进一个本地文件(FilePath)。 Workers 的文件系统是临时的——PUT 写完,下次调用就没了。 这是 serverless 化唯一必须改的东西,其余(路由、认证、校验、错误体)逐条照搬。

存储选 KV 不选 D1就一个 key、读多写少D1 是杀鸡用牛刀。 ⚠️KV 是最终一致的,写完立刻在别的边缘节点读可能拿到旧值(秒级窗口)。 同节点写后读一致,自己验证基本落在同节点,实际影响很小。


行为对照(逐条钉在 test/behaviour.test.ts 里)

场景 响应 备注
GET 有数据 200 + 紧凑 JSONETagCache-Control 字段序 releasetest,内层 versiongameVersionmessageurl
HEAD 200无 bodyContent-Length 与 GET 相同 08-08 加
GETIf-None-Match 命中 304无 body仍带 ETag/Cache-Control 08-08 加
GET 空库 404 {"title":"版本信息不存在"} 🔧08-08 改(原 500
PUTAuthorization 401 {"title":"未授权"}WWW-Authenticate: Bearer 08-08 加头
PUTBearer 开头 401 {"title":"无效的验证信息"}…error="invalid_request" 前缀比较大小写不敏感
PUT token 不匹配 401 {"title":"无效的密钥"}…error="invalid_token"
PUT JSON 语法坏 400 {"title":"无效的请求"} 🔧08-08 改(原 500
PUT 字段缺失 400 ValidationProblemerrors + title title = "key: 不能为空",多项用 ;
PUT 版本号格式非法 400 ValidationProblemerrors["release.version"] 🔧08-08 改(原 500 且不说哪个字段)
PUT 成功 204 无响应体,ETag 写完即知新标识,不必再 GET
路径不匹配 404 {"title":"无效的请求"}
OPTIONS 预检 204 + Allow-Methods/Allow-Headers/Max-Age 08-08 加
已注册路径 + 未注册动词 405 {"title":"无效的请求"},带 Allow: GET, HEAD, PUT 08-08 加 Allow

错误体是 ASP.NET 的 ProblemDetails 形状:typeRFC 9110 锚点)、titlestatus Content-Type application/problem+json

🔧 08-08 修掉的三条

原实现里这三种情况全部落进 UseExceptionHandler 变成 5xx—— 服务器没错却报服务器错,客户端据此重试也没有意义。

# 原来 现在 为什么
1 GET 空库 → 500「请求失败」 404「版本信息不存在」 资源不存在是 4xx5xx 会让客户端以为该重试
2 PUT JSON 坏 → 500「无效的请求」 400「无效的请求」 请求体坏是客户端的错
3 版本号格式非法 → 500不说哪个字段 400 ValidationProblem报到字段级 与"非空校验"共用一条出口,客户端能知道错在 release.version 还是 test.gameVersion

原实现未严格遵循 HTTP 语义,本次一并修正。

⚠️这三条是行为变更,不是等价迁移——切换后若有客户端依赖旧状态码,会在这里出问题。 现有客户端只读 GET 的 200 正常路径,不受影响;写入端只有你自己。

08-08 加的四项标准件

原 .NET 实现连 401 该带的 WWW-Authenticate 都没有。这轮补齐:

依据 对这个 API 的实际意义
WWW-Authenticate RFC 9110 §15.5.2 规定 401 必须带;参数格式 RFC 6750 §3 客户端能区分"没带凭据"与"凭据不对"
HEAD RFC 9110 §9.3.2:能 GET 的资源应能 HEAD 只查是否变化时不必拉 body
Cache-Control: public, max-age=60 版本 API 被反复轮询60 秒内直接用本地副本
ETag + If-None-Match RFC 9110 §8.8.3 / §13.1.2 未变化时回 304一个字节的 body 都不传
Cache-Control: no-cache RFC 9111 §5.2.2.4 "可以存但每次用前必须验"——⚠️不是"不缓存"

实现上的两个细节:

  • ETag 在 PUT 时算好存进 KV metadataGET 直接读——GET 是高频路径,不该每次重算哈希。 手工灌入的数据没有 metadata那时按内容现算兜底有用例锁着
  • If-None-Match 按标准处理了三种形状:*、逗号分隔列表、W/ 弱前缀。 这三条各自都能单独写错,所以各自有用例。

⚠️缓存策略最初写的是 public, max-age=60查客户端时改掉了 panel 保存完刷新页面60 秒窗口里浏览器直接吃本地副本 ⇒ 看到自己刚改掉的旧值。 换成 no-cache 后每次回来验一下,未变化仍是 304、body 不传,省流量的效果没丢。

🚨 迁移必看CORS 与预检08-08 核对客户端源码时发现)

线上那几个 access-control-* 头不是 .NET 应用给的——Program.cs 里没有 UseCors 它们是源站 nginx 加的。 Worker route 一拦截请求就不回源nginx 不参与,头跟着消失

后果比"少个头"严重得多:

  • panelhttps://mop.srcz.one)跨域调这个 API
  • 它的 PUT 带 Authorization + Content-Type: application/json ⇒ 浏览器必定先发 OPTIONS 预检
  • Worker 原先对 OPTIONS 返 405 ⇒ 预检失败PUT 一次都发不出去

⇒ Worker 自己实现了 CORS 与预检,允许的 origin 走 ALLOWED_ORIGINS 配置。 同源请求不带 Origin 头,自动不加 CORS 头——将来 panel 与 API 同域时这里不用动。

⚠️CORS 头贴在唯一出口fetch 的返回处),不散在各分支: 散着写必然漏掉某条错误路径,而漏掉的那条在浏览器里表现为 "请求失败但读不到状态码",是最难查的一类。错误响应带 CORS 有用例锁着。

🔑 这一条是读客户端源码 + 拉线上响应头才发现的—— 只读 API 自己的代码,它跑得好好的,什么都看不出来。

唯一有意偏离的一处

Token 比较从 string.Equals(Ordinal)(短路)换成恒定时间比较。 返回什么完全一致,只是不再泄漏"前几个字符对了"的时序信息。

已知的边界差异(实测,非推测)

url 字段两边都会做 URL 规范化(原实现是 new Uri(value).ToString())。 拿线上真实数据实测往返一致;下列形状两边行为也一致: 非 ASCII 路径转百分号编码、去掉默认端口 :443、空路径补 /、解析 ../

⚠️已知一处不同:形如 %41 这种"百分号编码的非保留字符" .NET 的 Uri.ToString() 会解码成 AJS 的 URL.toString() 保留原样。 线上数据里没有这种形状;真要用到,得单独处理。


部署

⚠️命令按 wrangler v3 写package.json 锁的是 ^3,实测装出 3.114.17)。 v3 的 kv key put 只有 --local、没有 --remote——不带 --local 默认就写远程。 若将来升到 v4这几条要重新对一遍照记忆写命令、不在真装的版本上跑一遍, 就是这一条把部署卡住的

cd worker
npm install

# 1. 建 KV 命名空间,把返回的 id 填进 wrangler.toml
npx wrangler kv namespace create VERSIONS

# 2. 写入 token不要写进 wrangler.toml凭据不进仓库
npx wrangler secret put TOKEN

# 3. 灌入现有数据(⚠️必做——否则 GET 会返 404
curl -s https://mov.srcz.one/api/v1/version > /tmp/versions.json
npx wrangler kv key put --binding=VERSIONS versions --path=/tmp/versions.json

# 4. 发布
npx wrangler deploy

路由为什么用 route 不用 custom domain

wrangler.toml 里配的是 pattern = "mov.srcz.one/api/v1/version*"。 custom domain 会把整个 mov.srcz.one 收进 Worker 而那台机上大概率还挂着别的 /api/* 服务——一绑就全抢走了。 route 只接管这一条路径,其余照旧回源。

⚠️切换前确认一下 mov.srcz.one 上还有哪些路径在用,别让 pattern 咬到别人。

回滚

删掉那条 route流量立刻回原来的源站KV 里的数据留着不动。 原 .NET 服务在切换稳定前不要停——这是回滚的唯一依靠。


测试

node --test test/behaviour.test.ts     # 37 条,无需 CF 账号
npx tsc --noEmit                       # 类型检查

回归锁钉的是对外行为,不是内部实现。 它防的是下一个人的顺手:契约靠人记住是记不住的,改坏了要当场红, 而不是等客户端报上来。

⚠️08-08 改那三条状态码时,这套锁当场红了 5 条3 条行为变更 + 2 条函数签名变更), 一条不漏——这就是它在干活的证据。改完再逐条更新用例,用例名里写清"维护者" 让"为什么与 .NET 版不同"这件事在代码里可追,而不是只活在聊天记录里。


客户端影响评估08-08 逐仓读过源码,非推测)

客户端 调用方式 受影响吗
mo4-version-panelSvelte/Astromop.srcz.one 浏览器 fetchGET + 带 Token 的 PUT 🔴 CORS 与预检必须补(见上);🟡 缓存策略已为它改成 no-cache
mo4-version-funcRGSSTelemetry.NET HttpClient.GetFromJsonAsync 🟢 不受影响

func 为什么安全(逐条核过,不是"应该没事"

  • HttpClient 默认不做 HTTP 缓存,也不发条件请求 ⇒ 拿不到 304Cache-Control 对它无意义
  • GetFromJsonAsync 对非 2xx 一律抛 HttpRequestException,调用方 ContinueWith 里只 Console.WriteLine ⇒ GET 空库从 500 改 404它的行为完全一样(都是静默跳过更新检查)
  • 非浏览器 ⇒ CORS 不适用

panel 对状态码改动是兼容且更好的: 错误分支读 res.statusText || data.title,而 400 ValidationProblem 的 title 现在是 "release.version: 版本号格式无效"——比原来那句笼统的「无效的请求」精确得多。