多应用同域部署下 Favicon 异常问题排查与治理

一、背景

同一对外域名下承载多个前端应用:

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 和缓存头。
相关推荐
YIAN1 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp
Imchendiana1 小时前
《狂人日记NO.9》— 前后端一把梭,我的全栈实录
前端·后端
逍遥运德1 小时前
前端工程化-03:包管理NPM-package.json 和 package-lock.json 详解
前端·前端框架·前端工程化
逗逗豆巴吧1 小时前
WPS 加载项中实现当前文档直接上传
前端·javascript·ecmascript 6
慢功夫1 小时前
💡【调试】按下 F5 后,mini-vscode 到底做了什么
前端·visual studio code
妙码生花1 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(二十一):网络请求封装优化
前端·javascript·vue.js
用户11481867894841 小时前
普通Fetch、Fetch流式、Axios、SSE:谁才是流式输出的正确操作?
前端·javascript
奇奇怪怪的1 小时前
多模态 RAG:图片与表格检索
前端
犇驫聊AI1 小时前
Claude Code 用了大半年才悟出来的 6 个技巧,第 3 个直接省一半 Token
前端·claude