工程与系统

Cloudflare 发布没卡在代码,而是卡在我误判了运行时边界

这不是一篇部署教程,而是一次边界误判复盘:proxy/middleware 承担了不该承担的职责,直到 build:cf 把问题撕开。

Cloudflare / OpenNext 这条部署链路最容易让人误判的地方,不是它完全不能跑,而是它会先给你很多“好像已经没问题”的信号。本地 next build 过了,测试也绿了,页面能打开,预览链路也看起来顺畅。直到真正跑到目标平台检查,npm run build:cf 才把那个一直被本地链路遮住的边界暴露出来:某些在 Node 语境里自然成立的写法,到了 Cloudflare / OpenNext 组合里并不成立。

最危险的不是红灯,而是一串不完整的绿灯

如果一条发布链路一开始就红得很彻底,反而好处理。你知道它还没准备好,也不会误把它推近发布。真正危险的是前面很多检查都通过,只有目标平台那一层还没有被纳入日常门槛。

这个项目里最典型的一次阻塞,来自 src/proxy.ts。本地构建可以通过,常规 release gate 也没有立刻把它拦下,但切到 Cloudflare / OpenNext 的真实构建时,问题就变得很明确:当前 proxy / middleware 形态不适合继续承担这部分职责。

这种误判很常见。你看到的是绿灯,实际得到的是局部绿灯。它证明当前环境下某些假设成立,却没有证明目标部署环境也接受这些假设。

proxy/middleware 的问题,是职责和运行时绑得太紧

src/proxy.ts 原本承担的事情并不复杂:旧路由跳转、安全响应头、一些全局性的请求处理。放在传统 Next.js 语境里,这种写法看起来很自然,因为 middleware/proxy 像一个方便的全局入口。

但方便不等于稳定。只要这层逻辑绑定了某个运行时能力,它就会在目标平台不支持时变成发布门槛。问题不再是“这一段代码有没有 bug”,而是“这一段职责是否应该继续放在这里”。

/projects -> /workspace 不需要靠 proxy 证明自己

旧项目入口跳转本质上是路由关系。它不需要在请求运行时里动态判断,也不应该为了一个稳定跳转去引入部署边界风险。把它迁回 next.config.mjs 的 redirects,是更清楚的职责归位。

安全响应头也不该绑死在代理层

安全响应头同样如此。如果它们是全局、静态、可配置的,就应该优先放在 headers 配置里,而不是依赖 proxy 在运行时补上。这样做不是为了少写代码,而是为了减少“运行时兼容”这个失败面。

删除 src/proxy.ts 比兼容它更诚实

最后真正让链路稳定的,不是把 proxy 硬改成另一种写法,而是承认它不该继续存在。redirects 和 headers 回到 next.config.mjs,回归测试直接锁住:src/proxy.ts 不应再出现,关键安全头和旧路由跳转必须留在配置层。

我后来把兼容性拆成三张表

这次之后,我不再把 Cloudflare 兼容性看成一个模糊清单,而是拆成三张更实用的表。

第一张是运行时边界表。哪些逻辑默认需要 Node,哪些 API 在边缘环境里不成立,哪些依赖虽然能安装但行为假设传统服务端存在。它的作用是阻止“本地能跑所以线上也能跑”的惯性。

第二张是职责归位表。不是所有曾经放在 middleware / proxy 里的逻辑都属于 middleware / proxy。跳转、headers、route handler、页面层、server action,各自应该承担什么,要比“哪里写起来快”更重要。

第三张是失败信号表。哪些失败必须在发布前暴露,哪些失败可以留到人工验收,哪些失败一旦线上出现成本就很高。build:cf 被纳入硬检查,就是因为它对应的是最昂贵的误判:你以为已经 ready,目标平台却并不接受。

release gate 应该先拦最贵的失败

我现在更愿意让 release gate 先回答一个不那么令人愉快的问题:最贵的失败会不会被及时发现?

对这个项目来说,普通测试、lint、本地 build 当然重要,但它们不能替代目标平台链路。Cloudflare / OpenNext 的失败如果只在最后一刻才出现,就说明门槛设置得太晚。真正稳的流程不是把所有检查堆成仪式,而是把最容易误判的那一层提前。

构建脚本不是装饰,它定义了发布现实

npm run build:cf 之所以应该存在,不是为了让脚本列表看起来专业,而是为了把“目标平台到底接不接受当前结构”变成一个可重复问题。只要这一步没有进入日常验证,所有前面的绿灯都只是部分事实。

Cloudflare / OpenNext 不是坏选择。它真正要求的是你别把传统 Node 语境里的顺手写法,默认为目标平台也会接受。越早把边界写进配置、测试、脚本和文档,发布就越像工程;越晚承认这些边界,部署就越像碰运气。