每次部署都把 D1 绑定抹掉——声明式配置的「未声明即删除」
灵犀官网的领码接口恒返回 1101。根因是 wrangler pages deploy 会把 Pages 项目的绑定同步为它对配置文件的解析结果,而那个项目的配置文件里没有绑定——绑定是在 dashboard 上挂的,于是每次部署都被同步成 None。这篇讲这个行为为什么是对的、两条出路各自的代价,以及同一台 Cloudflare 账号下另一个 Pages 项目为什么完全不受影响。
灵犀互联 Lynsi 上架时做了个小红书首发活动,官网加了个自助领码页。接口是 Pages Function,数据存 D1。
上线后接口恒定返回 1101。1101 是 Cloudflare Workers 运行时抛异常的错误码,这里的异常是 D1 绑定不存在——代码里的 env.REDEEM_DB 是 undefined。
而绑定明明是挂着的。去 dashboard 上看,D1 绑定在那儿,名字对、数据库对。重新挂一次,接口立刻通。然后部署一次,又坏了。
pages deploy 会同步绑定,不只是上传文件
实测结论(wrangler 4.123 与 4.127 表现一致):
wrangler pages deploy 每次都会把 Pages 项目的绑定配置,同步为它对 wrangler.toml 的解析结果。
而那个项目的 wrangler.toml 里从来没有绑定——绑定是当初在 dashboard 上手点的。于是 wrangler 每次解析出「无绑定」,然后忠实地把线上同步成无绑定。
这不是 bug。这是声明式配置的标准语义:配置文件是唯一真相,未声明即删除。 Terraform 是这样,Kubernetes 的 apply 是这样,wrangler 也是这样。会咬人的地方在于:
- dashboard 提供了一个看起来是持久化的编辑界面
- 而 CLI 部署会静默地把它覆盖掉,不告警、不确认、不在输出里提一句
两个入口对同一份状态有不同的真相来源,而其中一个会无声地赢。
同一个账号下,另一个 Pages 项目完全不受影响
这个站(mahui.me)本身也是 Pages 项目,也用 D1(评论)和 KV(留言)。今天我在它上面跑了四次 wrangler pages deploy,D1 接口一直是 200。
差别只有一处——它的绑定写在配置文件里:
{
"name": "mahui-me",
"pages_build_output_dir": "dist",
"kv_namespaces": [
{ "binding": "FEEDBACK", "id": "8ff90a5612d0474bb7078f815666ee04" }
],
"d1_databases": [
{ "binding": "COMMENTS", "database_name": "mahui-comments", "database_id": "..." }
]
}
wrangler 解析出这两个绑定,同步的结果和线上一致,什么都没变。
所以这个坑的触发条件是「绑定只存在于 dashboard」,不是 wrangler 有问题。同一个工具、同一个命令,在两个项目上表现相反,区别全在配置文件里有没有写。
两条出路
出路一:把绑定写进配置文件
最直接。上面 mahui.me 就是这么做的,而且它有额外好处:绑定进了版本控制,谁改的、什么时候改的都有记录,新克隆一份仓库也不会漏配。
代价是 database_id 这类标识要落进仓库。它不是密钥(没有它也访问不了,鉴权靠账号),所以这个代价通常可以接受。
出路二:接口迁到独立 Worker
灵犀选了这条。原因是那个项目的 Pages 配置已经被别的工具链管着,而且Workers 的绑定走自己的 deploy 通道,没有这个同步行为——一次配好就稳定。
name = "lynsi-redeem"
main = "src/index.js"
compatibility_date = "2026-08-01"
workers_dev = false
[[d1_databases]]
binding = "REDEEM_DB"
database_name = "lynsi-redeem"
database_id = "7f0ca9c8-fcd7-413d-aeb5-29bc25926207"
死掉的 Pages Function 删掉,Pages 的 toml 里留一条注释防止后人把绑定加回来:
领码接口不在这里:wrangler 的
pages deploy会把 Pages 项目的绑定同步为它对本文件的解析结果,而它始终解析出「无绑定」——每次部署都会抹掉 dashboard/API 挂上的 D1 绑定。不要在本文件加任何绑定。
这条注释是这次修复里最值钱的一行。半年后没人记得为什么领码接口在一个奇怪的地方,而「把它挪回 Pages Function 更整洁」是个非常自然的念头。
迁过去之后踩的三个坑
一、Worker 部署会向上找到 Pages 的配置
在 redeem-worker/ 里直接跑 wrangler deploy,wrangler 会向上级目录搜索配置文件,找到官网那份 Pages 的 wrangler.toml,然后用它去部署。
必须显式指定:
npx wrangler deploy --config wrangler.toml
一个把「就近配置」当便利的设计,在嵌套项目里就变成了陷阱。
二、workers_dev 必须写在所有 [[表]] 之前
TOML 的语法规则:表头之后的所有键值对都属于那个表。所以这样写是错的:
[[d1_databases]]
binding = "REDEEM_DB"
...
workers_dev = false # 这行成了 d1_databases 的字段,不是顶层配置
workers_dev = false 必须在任何 [[...]] 之前。写错的后果不是报错,是这个设置被静默忽略——*.workers.dev 入口照样开着。
关掉它的原因很实际:*.workers.dev 在国内解析和连通都不稳,留着只是一个用户点不开的旁门。
三、跨域绕了一圈又绕回来
第一版是页面在 Pages 域、接口在 Worker 域,跨域调用 + CORS 白名单(限定官网域和 pages.dev 预览域)。能跑,但多了 OPTIONS 预检,而且白名单是一处要跟着环境改的配置。
最终改成把 Worker 挂在官网同一域名的路径下:
[[routes]]
pattern = "lynsi.app.mahui.me/api/redeem*"
zone_name = "mahui.me"
实测 Worker 路由优先于 Pages 自定义域:/api/redeem* 由这个 Worker 接管,站内其余路径仍归 Pages。同源之后跨域和预检一起消失,还顺带解决了可用性——官网能开,接口就能开,不再依赖第二个域名的连通性。
这条比 CORS 那版好在:少一个可以单独坏掉的东西。
怎么早点发现
这个 bug 的形态是「修好之后会自己坏回去」,而两次「坏」之间隔着一次部署。如果修完就手测、然后过几天再部署,故障和操作之间的因果链就断了,很容易归因成「Cloudflare 抽风」。
有两个便宜的做法:
部署后跑一条冒烟检查,而不是部署前。 部署是最可能破坏运行时配置的动作,验证要在它之后。这次的检查很简单——GET 一下返回 remaining=100,POST 一下能领到真实链接、同 IP 重复领取幂等返回同一条。
把「在 dashboard 上改了什么」当成待办,不当成完成。 dashboard 是很好的探索和救火界面,但在 dashboard 上做的任何改动,都应该在当天回写进配置文件。否则它的寿命就是「到下一次 CLI 部署为止」。
一句话
声明式部署工具的「同步」意味着删除。 当一个工具声称它让线上状态匹配你的配置文件时,它同时也承诺了:你的配置文件里没写的东西,它会拿掉。 这个承诺才是重点,而它通常不会写在你正在读的那段文档里。
留言