fix(gateway): auth 类路由重复写 /favicon.ico 导致 nginx -t 失败(v96 部署被回滚的真因)

v96 部署时脚本报"网关配置重新生成失败 → 回滚到 ui-v95"。根因:auth 类路由的 Upstream
是在 writeErrorPages 之后才赋值的,所以它先按"门户自己渲染"拿到了门户 favicon,
随后新的"favicon 直通上游"块又写了同一个 location → nginx 拒绝重复 location。

修法:直通块对 auth 类跳过(门户页/登录页继续用门户自己的图标)。
另加回归测试 TestNoDuplicateExactLocations:解析渲染结果,断言任一 server 块内
不得出现重复的 `location = X`;已用"临时移除守卫"做过反向对照,确认该测试真能抓到。
This commit is contained in:
HomeDash Bot
2026-09-13 08:58:41 +08:00
parent 9c0d9f2bb5
commit ca80ba2510
2 changed files with 45 additions and 1 deletions

View File

@ -311,7 +311,10 @@ func writeRouteServer(b *strings.Builder, cfg *GatewayConfig, r Route) error {
// ② 导航项/历史条目的图标抓取依赖它在未登录时可取。
// 之前这里是"在门户 root 里 try_files",等于把每个被代理站点的图标都换成门户图标
// (实测 pi / ittools / vikunja 存下了同一张门户图标)。
if up := strings.TrimSpace(r.Upstream); up != "" {
// 注意:auth 类的上游是在上面 writeErrorPages 之后才赋值的,那时它已按"门户自己渲染"
// 拿到了门户 favicon —— 这里必须跳过,否则同一个 location 出现两次,nginx -t 直接失败
// (部署脚本回滚过一版,实测踩到)。
if up := strings.TrimSpace(r.Upstream); up != "" && routeKind(r) != "auth" {
for _, fa := range []string{"/favicon.ico", "/favicon.svg"} {
fmt.Fprintf(b, " location = %s {\n auth_request off;\n proxy_pass %s;\n", fa, up)
b.WriteString(" proxy_set_header Host $host;\n")

View File

@ -3,6 +3,7 @@ package main
import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)
@ -534,3 +535,43 @@ func TestProxyRoutePassesFaviconThrough(t *testing.T) {
t.Errorf("门户块应保留门户自己的 favicon:\n%s", portalBlock)
}
}
// 同一 server 块内不得出现重复的 "location = X"。
// 实测踩到:auth 类路由的上游在 writeErrorPages 之后才赋值,导致
// /favicon.ico 被写两次 → nginx -t 失败 → 部署脚本回滚。
func TestNoDuplicateExactLocations(t *testing.T) {
out, err := RenderGatewayConf(&GatewayConfig{
ListenPort: 3081,
AuthURL: "http://127.0.0.1:3005/auth/verify",
LoginURL: "https://login.example/login",
Routes: []Route{
{Host: "portal.example", Kind: "portal"},
{Host: "auth.example", Kind: "auth"},
{Host: "plain.example", Upstream: "http://127.0.0.1:8080"},
{Host: "guarded.example", Upstream: "http://127.0.0.1:9090", RequireLogin: true,
Websocket: true, BypassPaths: []string{"/search", "/api/"}},
{Host: "tls.example", Upstream: "https://192.0.2.10:5667", InsecureTLS: true},
},
})
if err != nil {
t.Fatal(err)
}
for i, block := range strings.Split(out, "server {") {
if !strings.Contains(block, "server_name ") {
continue
}
seen := map[string]int{}
for _, m := range regexp.MustCompile(`(?m)^\s*location\s+=\s+(\S+)\s*\{`).FindAllStringSubmatch(block, -1) {
seen[m[1]]++
}
for loc, n := range seen {
if n > 1 {
t.Errorf("第 %d 个 server 块里 location = %s 出现 %d 次(nginx 会拒绝)", i, loc, n)
}
}
}
// 代理块应把 favicon 直通上游;auth/portal 块保留门户图标
if !strings.Contains(out, "location = /favicon.ico") {
t.Error("完全没有 favicon location")
}
}