fix(status): 修掉 Dashy 可达性检查"全黄"——改用免认证探测路径
用户反馈可达性全黄。实测:appConfig.statusCheck 已开启,但等 6 秒仍无状态 节点渲染;上游直连全部 200。机制:Dashy 用 no-cors 探测条目 URL,而受保护域名 会 302 跳到另一个域(登录页),no-cors 下跨域重定向直接报错 → 检查永远停在 "进行中" → 全黄。 修法: - gateway 为每条路由生成 location = /.navi-ping:auth_request off + 反代到 真实上游(上游挂了返回 5xx → 红;正常 2xx → 绿),portal 类型直接 204 - 导航同步时设置 NavItem.StatusCheck = true 与 statusCheckUrl 指向该路径 - 注意:statusCheck 是 NavItem 的结构体字段,不能塞进 inline Extra(yaml 会 panic: conflicts with struct field)——已改为设置字段本身 测试:新增断言——探测路径存在、免认证、反代到真实上游、portal 返回 204。
This commit is contained in:
@ -257,6 +257,8 @@ func writeRouteServer(b *strings.Builder, cfg *GatewayConfig, r Route) error {
|
||||
writeErrorPages(b, routeKind(r) == "portal")
|
||||
|
||||
if routeKind(r) == "portal" {
|
||||
// 静态门户页也要给出确定性探测结论(免认证,避免 302 到登录页)
|
||||
b.WriteString(" location = /.navi-ping { auth_request off; access_log off; return 204; }\n")
|
||||
b.WriteString(" index index.html;\n location / { try_files $uri $uri/ /404.html; }\n}\n\n")
|
||||
return nil
|
||||
}
|
||||
@ -265,6 +267,19 @@ func writeRouteServer(b *strings.Builder, cfg *GatewayConfig, r Route) error {
|
||||
r.Upstream = "http://127.0.0.1:3005"
|
||||
r.RequireLogin = false
|
||||
}
|
||||
// 可达性探测路径:Dashy 用 no-cors 探测条目 URL,若命中统一登录的 302 跳转
|
||||
// (跳到另一个域)浏览器会直接报错,检查就永远停在"进行中"(表现为全黄)。
|
||||
// 这里给每条路由生成一个免认证的固定路径,并且**反代到真实上游**:
|
||||
// 上游正常则 2xx(绿),上游挂了则 5xx(红),语义正确。
|
||||
b.WriteString(" location = /.navi-ping {\n auth_request off;\n access_log off;\n")
|
||||
if routeKind(r) == "portal" {
|
||||
b.WriteString(" return 204;\n }\n")
|
||||
} else {
|
||||
fmt.Fprintf(b, " proxy_pass %s/;\n", strings.TrimRight(strings.TrimSpace(r.Upstream), "/"))
|
||||
b.WriteString(" proxy_set_header Host $host;\n")
|
||||
b.WriteString(" proxy_connect_timeout 4s;\n proxy_read_timeout 4s;\n")
|
||||
b.WriteString(" proxy_redirect off;\n }\n")
|
||||
}
|
||||
if r.RequireLogin {
|
||||
if strings.TrimSpace(cfg.AuthURL) == "" || strings.TrimSpace(cfg.LoginURL) == "" {
|
||||
return fmt.Errorf("%s: 需要认证但未配置认证/登录地址", host)
|
||||
|
||||
@ -104,7 +104,9 @@ func syncGatewayNavigation(gw *GatewayConfig) error {
|
||||
if icon == "" {
|
||||
icon = "fas fa-globe"
|
||||
}
|
||||
statusOn := true
|
||||
item := NavItem{
|
||||
StatusCheck: &statusOn,
|
||||
Title: strings.TrimSpace(r.NavName),
|
||||
Description: strings.TrimSpace(r.NavDescription),
|
||||
Icon: icon,
|
||||
@ -113,6 +115,9 @@ func syncGatewayNavigation(gw *GatewayConfig) error {
|
||||
gatewayManagedKey: true,
|
||||
gatewayHostKey: strings.ToLower(strings.TrimSpace(r.Host)),
|
||||
gatewayAutoIconKey: r.NavAutoIcon,
|
||||
// 可达性检查指向网关生成的免认证路径:避免命中登录 302 导致
|
||||
// no-cors 探测报错、状态永远"进行中"(用户反馈的"全黄")。
|
||||
"statusCheckUrl": "https://" + strings.TrimSpace(r.Host) + "/.navi-ping",
|
||||
},
|
||||
}
|
||||
dashy.Sections[idx].Items = append(dashy.Sections[idx].Items, item)
|
||||
|
||||
@ -358,3 +358,29 @@ func TestRenderGatewayConfErrorPagesSkipAuth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 每条路由都要有免认证探测路径,且必须反代到真实上游(否则无法反映服务健康)。
|
||||
func TestRenderGatewayConfPingPath(t *testing.T) {
|
||||
out, err := RenderGatewayConf(&GatewayConfig{ListenPort: 8081, AuthURL: "http://x/_auth",
|
||||
LoginURL: "https://l/login",
|
||||
Routes: []Route{
|
||||
{Host: "a.com", Upstream: "http://10.0.0.1:3000", RequireLogin: true},
|
||||
{Host: "p.com", Kind: "portal"},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("渲染失败: %v", err)
|
||||
}
|
||||
if !contains(out, "location = /.navi-ping {") {
|
||||
t.Fatalf("缺少探测路径:\n%s", out)
|
||||
}
|
||||
if !contains(out, "auth_request off;") {
|
||||
t.Fatalf("探测路径必须免认证")
|
||||
}
|
||||
// 普通路由的探测要打在真实上游上(挂了要能红)
|
||||
if !contains(out, "proxy_pass http://10.0.0.1:3000/;") {
|
||||
t.Fatalf("探测未反代到上游:\n%s", out)
|
||||
}
|
||||
if !contains(out, "return 204;") {
|
||||
t.Fatalf("portal 类型应直接 204")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user