一、背景
同一对外域名下承载多个前端应用:
txt
https://example.com/app-a/ -> 应用 A
https://example.com/app-b/ -> 应用 B
https://example.com/ -> 默认应用或门户服务
外层网关按路径前缀转发到不同服务:
css
location /app-a/ {
proxy_pass http://APP_A_SERVICE/;
}
location /app-b/ {
proxy_pass http://APP_B_SERVICE/;
}
location / {
proxy_pass http://DEFAULT_SERVICE;
}
问题发生在应用 B 中:应用首页 favicon 正常,但点击 PDF 文件后,浏览器内置 PDF 预览页的页签 icon 与应用 B 的 icon 不一致。
二、问题现象
应用 B 首页:
arduino
https://example.com/app-b/portal/home
应用 B favicon:
arduino
https://example.com/app-b/favicon.ico
PDF 文件资源:
arduino
https://example.com/app-b/files/xxx.pdf
现象:
css
1. 应用 B 首页页签 icon 正常。
2. PDF 新窗口预览时,页签 icon 显示为旧版本或默认服务 icon。
3. Network 中可以看到浏览器不仅请求 /app-b/favicon.ico,还会请求 /favicon.ico。
三、根因分析
PDF 文件直开时,浏览器打开的不是前端 SPA 页面,而是浏览器内置 PDF Viewer。
PDF 预览页不会加载应用 B 的 index.html,因此不会读取应用 B 页面里的声明:
ini
<link rel="icon" href="/app-b/favicon.ico" />
Chrome 内置 PDF Viewer 会额外请求站点根路径 favicon:
arduino
https://example.com/favicon.ico
如果外层网关没有显式处理 /favicon.ico,该请求会落入默认路由:
arduino
location / {
proxy_pass http://DEFAULT_SERVICE;
}
当默认服务返回旧 favicon 时,PDF 预览页签就会显示旧 icon。
四、Favicon 请求为什么特殊
普通业务页面通常能按路径前缀分流:
bash
/app-a/* -> 应用 A
/app-b/* -> 应用 B
但 favicon 有两个特殊点:
bash
1. 页面声明的 favicon 可能是 /app-b/favicon.ico。
2. 浏览器仍可能自动请求根路径 /favicon.ico。
尤其是 PDF Viewer、浏览器历史缓存、书签、页签图标刷新等场景,根路径 favicon 的影响会非常明显。
因此在多应用同域部署下,不能只保证应用前缀下的 favicon 正确,还要治理根路径 favicon:
bash
/favicon.ico
五、重定向与反向代理的区别
排查过程中容易混淆两种 Nginx 行为:重定向和反向代理。
5.1 重定向
重定向是让浏览器自己换地址再请求。
常见配置:
bash
location = /favicon.ico {
return 302 http://example.com:PORT/app-b/favicon.ico;
}
或:
bash
rewrite ^/favicon.ico http://example.com:PORT/app-b/favicon.ico redirect;
浏览器访问:
arduino
https://example.com/favicon.ico
Nginx 返回:
bash
HTTP/2 302
Location: http://example.com:PORT/app-b/favicon.ico
含义是:
你不要找我了,自己去 Location 里的新地址请求。
此时浏览器会真的去请求:
bash
http://example.com:PORT/app-b/favicon.ico
风险:
markdown
1. 内部端口暴露给浏览器。
2. HTTPS 页面跳到 HTTP 资源,会触发 mixed content。
3. 内部端口公网不可达时,请求会超时。
判断是否发生重定向,重点看响应头:
makefile
HTTP 301 / 302 / 307 / 308
Location: ...
5.2 反向代理
反向代理是 Nginx 自己去后端服务取资源,再返回给浏览器。
配置示例:
ini
location = /favicon.ico {
proxy_pass http://INTERNAL_SERVICE/favicon.ico;
}
浏览器访问:
arduino
https://example.com/favicon.ico
Nginx 在服务器内部请求:
arduino
http://INTERNAL_SERVICE/favicon.ico
然后把结果返回给浏览器。
浏览器看到的地址始终是:
arduino
https://example.com/favicon.ico
它不会知道后端真实地址和端口。
正常响应类似:
arduino
HTTP/2 200
Content-Type: image/x-icon
没有 Location。
5.3 一句话区别
ini
return 302 / rewrite redirect = 让浏览器换地址去请求
proxy_pass = Nginx 自己去后端拿,浏览器地址不变
在公网入口网关中,不应该把浏览器重定向到内部端口。
内部服务地址只能由网关通过 proxy_pass 访问。
六、排查过程中的关键判断
6.1 不能只看 HTTP 状态码
曾出现过:
arduino
HTTP/1.1 200 OK
Content-Type: text/html
这对 favicon 来说不是成功,而是返回了 HTML,通常是 SPA 的 index.html 兜底页。
favicon 正常响应应类似:
arduino
HTTP/2 200
Content-Type: image/x-icon
6.2 注意错误跳转
如果看到:
bash
HTTP/2 302
Location: http://example.com:PORT/...
说明发生了错误跳转。
这类跳转通常会导致:
markdown
1. 内部端口暴露。
2. HTTPS 页面加载 HTTP 资源,被浏览器拦截。
3. 页签 icon 请求失败。
6.3 注意 mixed content
如果页面是:
arduino
https://example.com/app-b/portal/home
但 favicon 跳到了:
bash
http://example.com:PORT/app-b/favicon.ico
浏览器会阻止该资源加载,并显示:
makefile
blocked:mixed-content
这不是前端代码问题,而是资源跳转协议和入口不一致。
6.4 注意 proxy_pass 尾部斜杠
下面两种配置含义不同:
arduino
proxy_pass http://APP_B_SERVICE/;
和:
arduino
proxy_pass http://APP_B_SERVICE;
是否带尾部 / 会影响 URI 转发结果。
在多应用前缀部署中,是否保留 /app-b/ 前缀会影响后端服务能否正确命中静态资源。
七、短期修复方案
在外层 443 网关中显式处理根 favicon:
ini
location = /favicon.ico {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0" always;
proxy_pass http://STATIC_ICON_SOURCE/favicon.ico;
}
其中 STATIC_ICON_SOURCE 应指向一个明确的新 icon 来源,例如:
markdown
1. 外层网关本机静态文件。
2. 已确认返回新 icon 的内部静态服务。
3. 统一静态资源服务。
要求最终响应必须是:
arduino
HTTP/2 200
Content-Type: image/x-icon
不能出现:
makefile
Location: http://...
Location: ...:内部端口...
Content-Type: text/html
八、应用自身 Favicon 的处理
应用 B 页面自身通常会请求:
arduino
https://example.com/app-b/favicon.ico
该路径理论上应该由应用 B 服务自己处理,而不是长期在外层网关堆精确规则。
推荐在应用 B 服务侧正确处理:
bash
location = /app-b/favicon.ico {
alias /path/to/app-b/dist/favicon.ico;
add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0" always;
}
或根据应用 B 的实际部署方式,让以下路径至少有一个能稳定返回新 icon:
bash
/app-b/favicon.ico
/favicon.ico
但外层网关对 /app-b/ 的代理规则必须与应用 B 的实际前缀处理保持一致。
九、推荐的长期治理方案
最终应形成清晰职责边界:
bash
/favicon.ico -> 外层网关统一控制,作为该域名的全站默认 favicon
/app-a/favicon.ico -> 应用 A 自己控制
/app-b/favicon.ico -> 应用 B 自己控制
/app-a/* -> 应用 A 服务
/app-b/* -> 应用 B 服务
/ -> 默认应用或门户服务
推荐外层网关只特殊处理:
ini
location = /favicon.ico {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0" always;
proxy_pass http://STATIC_ICON_SOURCE/favicon.ico;
}
不建议长期在外层网关中堆叠:
bash
/app-a/favicon.ico
/app-b/favicon.ico
这些路径应回到各自应用服务内处理。
十、验证命令
验证根 favicon:
arduino
curl -I https://example.com/favicon.ico
验证应用 favicon:
arduino
curl -I https://example.com/app-b/favicon.ico
期望结果:
arduino
HTTP/2 200
Content-Type: image/x-icon
如果看到以下情况,需要继续排查。
返回 HTML:
arduino
Content-Type: text/html
说明返回的是 HTML,不是 icon。
发生跳转:
bash
HTTP/2 302
Location: http://example.com:PORT/...
说明发生了错误重定向,可能暴露内部端口或触发 mixed content。
浏览器阻止:
makefile
blocked:mixed-content
说明 HTTPS 页面加载了 HTTP 资源,需要修正跳转或代理链路。
十一、本次问题暴露出的知识点
11.1 同域不等于同一个前端应用
同一个域名下可以挂载多个系统。
arduino
https://example.com/app-b/portal/home
https://example.com/app-b/files/xxx.pdf
前者是前端 SPA 页面,后者是文件资源。
文件资源不会加载前端应用的 index.html。
11.2 PDF Viewer 不受前端 favicon 声明控制
PDF 直开时,顶层页面是浏览器内置 PDF Viewer。
它不会读取业务前端里的:
ini
<link rel="icon" href="/app-b/favicon.ico" />
因此需要保证根路径:
bash
/favicon.ico
也返回正确 icon。
11.3 200 不一定代表资源正确
200 text/html 对 favicon 来说仍然是错误。
必须同时关注:
css
Status
Content-Type
Content-Length
Location
11.4 多层 Nginx 要分清请求命中哪一层
常见层级:
rust
浏览器
-> 外层 443 网关
-> 应用服务 Nginx
-> 前端静态文件
排查时需要逐层验证,而不是只看某一份配置。
11.5 外层网关不应该把浏览器重定向到内部端口
错误示例:
bash
Location: http://example.com:PORT/xxx
正确方式是外层网关内部代理:
arduino
proxy_pass http://INTERNAL_SERVICE;
浏览器始终只看到:
arduino
https://example.com/...
十二、总结
本次问题的本质不是 favicon 文件没有替换,而是:
bash
同域多应用部署下,浏览器根路径 /favicon.ico 请求没有被网关显式治理,导致 PDF Viewer 拿到了默认服务的旧 icon。
根治思路是:
markdown
1. 外层网关统一治理 /favicon.ico。
2. 各应用自行治理各自前缀下的 favicon。
3. 禁止 favicon 请求跳转到 HTTP 或内部端口。
4. 验证时同时检查 Content-Type、Location 和缓存头。