三层断点:新功能上线的"最后一百米"
园区平台的消防设备地图功能上线那天,楼层平面图一张也显示不出来。从 dev 联调一路追到生产 Nginx,最后发现这不是一个 bug,而是三层断点叠在一起:库表迁移没跑、前端 env 缺配置、Nginx 缺路由。任何一层单独修好,页面依然是坏的——这大概是”新功能第一次把文件服务暴露到浏览器侧”时最典型的事故形态。

图 1:dev 环境里这三层各自被本地库、本地 env 覆盖文件、直连端口悄悄绕过,生产环境一个都绕不过。
故障时间线
- 前端的直觉:图片只有直连后端端口才能打开,同事问”不应该走网关吗?要不要配 Nginx?”——这个直觉问到了点子上,只是当时没人知道答案。
- 线上第一爆:
Table 'building_env.fire_map_plan' doesn't exist。新表压根没建,接口直接 500。 - 线上第二爆:表建好后图片 404,但 URL 已经变成生产入口地址——说明前端那层已经有人配过了,断点往后移到了 Nginx。
- 终验:Nginx 加完路由,
curl -I返回 200 image/jpeg,页面刷新可见。
第一问:这个绝对地址到底是谁拼的?
图片打不开,最忌讳一上来就改 Nginx。先分清 URL 是谁拼的——绝对地址只有三种来源:后端配置、后端代码、前端 env。逐一排除后,嫌疑落在前端组件的 computed 上:
1 | planImageUrl() { |
问题就出在最后这行:VUE_APP_IMGPATH 全项目只有这一处引用,而 .env.production、.env.development、.env.staging 全都没定义它。生产构建出来的地址是 undefined/fs/...,必然 404。
为什么 dev 联调时没人发现?因为每位前端同事本地都有一份不进 git 的 .env.development.local,各自配了后端地址。每个人本地各配各的,恰好集体掩盖了生产配置的缺失。 这是”三层断点”得以潜伏的土壤。
第二问:后端返回的到底是相对还是绝对路径?
前端拼前缀的前提是后端给相对路径。这个前提不能想当然,要实证。追进框架 jar(仓库无源码,反编译确认)后发现两点:
- 上传接口返回的
fileUrl就是/fs/...纯相对路径,环境无关; - 那个看起来很关键的
${icfg.common.fs-server}配置,注入之后零次引用——是死配置,配了也不会有任何效果。
再用数据库闭环:表里存的确实是 /fs/firePlan/20260814/xxx.png。数据链路从头到尾都是对的,断点纯粹在前端 env 与 Nginx 两处。
看到名字顺眼的配置项就想往配置中心里塞一个值,是一种本能。但”存在这个 @Value“不等于”有人读它”——本次的 fs-server 就是反编译确认过的死配置。改配置之前,先确认这行配置活着。
修复:三层各补一刀
第一层,建表。 本项目的 Flyway 不集成进服务、在服务器上以 CLI 单独执行,所以”代码发了但表没建”是常态而非意外。手动跑完迁移脚本即可,不需要重启——MyBatis 运行时查库,表一出现接口立即恢复。
第二层,前端 env。 .env.production 补上 VUE_APP_IMGPATH,让相对路径拼出生产同源前缀,重新构建。
第三层,Nginx 加路由。 生产实测拓扑是:入口是一台独立的 Nginx 机器,Java 服务在另一台机器上,跨机转发要写内网地址。在 location /gateway/ 旁边加:
1 | location /fs/ { |
nginx -t && nginx -s reload,真实请求打出 200,收工。
本次最大的坑:proxy_pass 末尾那根斜杠
proxy_pass 不带路径部分时,Nginx 把原始 URI 原样转发;一旦写成带斜杠的 http://host:18100/,Nginx 会把匹配到的 /fs 前缀剥掉再拼接到 / 后面——后端收到的是 /xxx,404。这是官方文档里写得明明白白、但几乎每个团队都要踩一次的行为。
更阴险的是:同一个配置文件里,location /gateway/ 那条是故意带斜杠的——它就是要剥掉 /gateway 前缀再交给网关。两条 location 需求恰好相反,照抄邻居的写法必踩坑。改 Nginx 配置时真正要紧的只有三件事:行尾分号、花括号闭合、proxy_pass 带不带斜杠。缩进和空格,Nginx 根本不在乎。
为什么不走网关?
<img> 标签发起的请求不带 Authorization 头,走网关会被鉴权拦截;要放行就得在鉴权白名单上开口子。而 /fs 免鉴权直连本就是框架的既有设计,Nginx 同源反代恰好是最贴合这个设计的做法——顺带还消掉了 canvas 的跨域污染问题。另外,Content-Disposition: attachment 只影响浏览器直接打开 URL 的场景(会变成下载),<img> 渲染不受任何影响。
方法论:三层断点模型
新功能第一次把文件服务暴露到浏览器侧时,”库表迁移 / 前端 env / Nginx 路由”这三层默认全是断的——dev 环境里它们各自被本地库、.env.development.local、直连端口绕过去了,生产一个都绕不过。只补一层就卡在下一层,所以上线清单要把三层一起列进去。
本次验证有效的排查顺序:
- 先分清 URL 是谁拼的(前端 env / 后端配置 / 框架代码),别急着动网络层;
- 查库,确认存的到底是相对还是绝对路径;
- 从最底层 curl 服务直连,证明”服务本身正常”;
- 沿链路逐层向上,每层用最小实验证明”这层通,还是不通”。
排障的本质不是猜,而是把一条长链路切成若干可以用一次 curl 判定生死的短段。
参考资料
- nginx: ngx_http_proxy_module — proxy_pass(带不带 URI 部分的替换语义,官方原文)
- MDN: CORS enabled image(
<img>与 canvas 跨域污染) - Vue CLI: 环境变量和模式(
.env.local不进 git 的约定,正是本次隐患的来源)