From ca80ba251029542691eb8017a5194ca60257c214 Mon Sep 17 00:00:00 2001 From: HomeDash Bot Date: Sun, 13 Sep 2026 08:58:41 +0800 Subject: [PATCH] =?UTF-8?q?fix(gateway):=20auth=20=E7=B1=BB=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E9=87=8D=E5=A4=8D=E5=86=99=20/favicon.ico=20=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=20nginx=20-t=20=E5=A4=B1=E8=B4=A5=EF=BC=88v96=20?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E8=A2=AB=E5=9B=9E=E6=BB=9A=E7=9A=84=E7=9C=9F?= =?UTF-8?q?=E5=9B=A0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v96 部署时脚本报"网关配置重新生成失败 → 回滚到 ui-v95"。根因:auth 类路由的 Upstream 是在 writeErrorPages 之后才赋值的,所以它先按"门户自己渲染"拿到了门户 favicon, 随后新的"favicon 直通上游"块又写了同一个 location → nginx 拒绝重复 location。 修法:直通块对 auth 类跳过(门户页/登录页继续用门户自己的图标)。 另加回归测试 TestNoDuplicateExactLocations:解析渲染结果,断言任一 server 块内 不得出现重复的 `location = X`;已用"临时移除守卫"做过反向对照,确认该测试真能抓到。 --- admin-server/gateway.go | 5 ++++- admin-server/gateway_test.go | 41 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/admin-server/gateway.go b/admin-server/gateway.go index ec6c083..0878bf4 100644 --- a/admin-server/gateway.go +++ b/admin-server/gateway.go @@ -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") diff --git a/admin-server/gateway_test.go b/admin-server/gateway_test.go index fe20e56..46ca2ec 100644 --- a/admin-server/gateway_test.go +++ b/admin-server/gateway_test.go @@ -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") + } +}