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") + } +}