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:
@ -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")
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user