这次发布真正卡住的时候,我第一反应不是“Cloudflare 又出了什么限制”,而是意识到前面那些绿灯给了我太多安全感。lint 过了,测试过了,本地 next build 过了,页面也能打开;如果只看这些信号,很容易相信版本已经接近可发。可正式目标不是本地 Node 运行时,而是 Cloudflare / OpenNext。只要目标平台那一关没有提前参与判断,前面再多绿灯也只能证明一半事实。
最危险的不是红灯,而是红灯来得太晚
工程问题最怕的不是一开始就失败。早失败反而干净:你知道版本还没准备好,也不会把它包装成“差不多能发”。真正麻烦的是一串局部绿灯把人推向过早结论,直到最后一步才暴露目标平台不接受当前结构。
这次阻塞就是这样出现的。项目的常规链路已经能证明代码在本地环境下健康,但它没有充分证明 Cloudflare / OpenNext 的目标链路也健康。于是发布判断被延后到了最不该延后的地方:准备上线的时候。
如果失败只在最后一公里出现,它就不只是一个技术错误,而是流程设计的问题。
`build:cf` 暴露的不是一个文件,而是一组错误假设
真正把问题掀开的,是 npm run build:cf。它把本地默认链路之外的目标平台约束拉进来,直接指向 proxy / middleware 这层不成立。
当时问题集中在 src/proxy.ts。这层原本放了两类看起来很自然的职责:旧路由 /projects -> /workspace 的跳转,以及统一安全响应头。单看每一项都不复杂,甚至都很适合被写成“全局处理”。但它们一旦被绑在一个平台兼容性敏感的中间层里,就会从便利入口变成上线风险。
legacy redirect 不是必须待在请求中间层
/projects -> /workspace 是稳定路由关系,不需要靠运行时代理证明自己。把它放回 next.config.mjs 的 redirects,更接近它的职责本质,也更容易被测试锁住。
安全响应头也不需要运行时动态处理
X-Content-Type-Options、X-Frame-Options、Referrer-Policy、Permissions-Policy 这些全局安全头,本身是稳定声明。既然它们不依赖请求时动态判断,就不应该为了统一而挤进 proxy。
删除 proxy 比兼容 proxy 更重要
最后真正让链路变稳的,不是给 proxy 找一种新姿势,而是承认这层不该继续存在。redirect 和 headers 回到配置层,src/proxy.ts 被删除,测试直接锁住这条边界。
我过去把“方便统一”误当成了“结构正确”
这次最值得保留的教训,是我对代理层的默认信任被打破了。proxy/middleware 很容易让人觉得它是一个自然的统一入口:路由、响应头、请求前处理、兼容逻辑都能塞进去。可它同时站在运行时、平台适配、缓存和请求边界上,天然就是最容易被部署目标放大的位置。
如果一个层级越靠近上线边界,它就越不应该承担太多职责。你给它塞得越多,出问题时越难判断到底是路由问题、安全头问题、平台运行时问题,还是框架版本问题。
边界层应该先被怀疑,而不是默认保留
我以后看这类层级会先问三个问题:这层是不是必须存在?它承担的职责有没有更低风险的位置?如果目标平台拒绝它,是否有明确回退路径?
这些问题比“怎么让它继续跑”更重要。因为很多上线阻塞并不是代码写错,而是职责被放在了一个不该被放大的地方。
release gate 应该锁住最贵的误判
以前我容易把发布门禁理解成“检查项足够多”。这次之后,我更愿意把它理解成“最贵的误判能不能提前暴露”。
对 MakePlans 来说,最贵的误判之一不是某个 UI 细节错了,而是错误地相信版本已经 ready。只要本地链路和目标平台链路不完全等价,目标平台检查就不能是附加项,而应该进入硬门槛。
next build 证明不了 Cloudflare readiness
本地 next build 仍然重要,但它只说明本地框架链路健康。build:cf 才能说明目标平台是否接受当前结构。两者不是互相替代,而是回答不同问题。
所以这次发布阻塞最后留下来的,不是一条“Cloudflare 注意事项”,而是一条更稳定的工程判断:凡是会阻断上线的目标环境边界,都必须比上线动作更早进入日常验证。绿灯只有覆盖了真正目标,才配叫可发布。