fix: harden health probe responses
This commit is contained in:
@@ -36,6 +36,8 @@ curl -i http://127.0.0.1:8080/health/ready
|
|||||||
{"status":"not_ready"}
|
{"status":"not_ready"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
健康检查响应带 `Cache-Control: no-store` 和 `X-Content-Type-Options: nosniff`,避免探针状态被缓存。`503` 会带 `Retry-After: 1`,提示路由层至少等待 1 秒后再探测;实际探测周期仍由部署平台配置决定。
|
||||||
|
|
||||||
响应不包含 DSN、数据库错误文本或凭据。检查迁移记录不能发现所有人工 DDL 漂移,运行账户权限和迁移流程仍需受控。
|
响应不包含 DSN、数据库错误文本或凭据。检查迁移记录不能发现所有人工 DDL 漂移,运行账户权限和迁移流程仍需受控。
|
||||||
|
|
||||||
进程收到 `SIGTERM` 后会先进入排空状态:就绪探针立即返回 `503`,再在有限的优雅关闭窗口内停止 HTTP/WS 服务并等待存量请求结束。存量任务租约不会因为探针变化而被删除,仍由正常恢复逻辑接管。
|
进程收到 `SIGTERM` 后会先进入排空状态:就绪探针立即返回 `503`,再在有限的优雅关闭窗口内停止 HTTP/WS 服务并等待存量请求结束。存量任务租约不会因为探针变化而被删除,仍由正常恢复逻辑接管。
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ func (r *Readiness) BeginDrain() {
|
|||||||
func New(addr string, ready Checker, tasks ...*service.Service) *http.Server {
|
func New(addr string, ready Checker, tasks ...*service.Service) *http.Server {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
write := func(w http.ResponseWriter, status int, body map[string]string) {
|
write := func(w http.ResponseWriter, status int, body map[string]string) {
|
||||||
|
// 探针响应不能被代理或浏览器缓存,否则下线节点可能继续被路由流量。
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
_ = json.NewEncoder(w).Encode(body)
|
_ = json.NewEncoder(w).Encode(body)
|
||||||
@@ -61,6 +64,8 @@ func New(addr string, ready Checker, tasks ...*service.Service) *http.Server {
|
|||||||
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
|
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if ready == nil || ready(ctx) != nil {
|
if ready == nil || ready(ctx) != nil {
|
||||||
|
// 给负载均衡器一个明确的短暂重试提示;真正的重试节奏仍由探针策略控制。
|
||||||
|
w.Header().Set("Retry-After", "1")
|
||||||
write(w, http.StatusServiceUnavailable, map[string]string{"status": "not_ready"})
|
write(w, http.StatusServiceUnavailable, map[string]string{"status": "not_ready"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ func TestHealthSeparatesLivenessAndReadiness(t *testing.T) {
|
|||||||
if rr.Code != want || strings.Contains(rr.Body.String(), "internal-database-details") {
|
if rr.Code != want || strings.Contains(rr.Body.String(), "internal-database-details") {
|
||||||
t.Fatalf("%s: %d %s", path, rr.Code, rr.Body.String())
|
t.Fatalf("%s: %d %s", path, rr.Code, rr.Body.String())
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(path, "/health/") {
|
||||||
|
if rr.Header().Get("Cache-Control") != "no-store" || rr.Header().Get("X-Content-Type-Options") != "nosniff" {
|
||||||
|
t.Fatalf("%s: missing anti-cache/security headers: %#v", path, rr.Header())
|
||||||
|
}
|
||||||
|
if path == "/health/ready" && rr.Header().Get("Retry-After") != "1" {
|
||||||
|
t.Fatalf("readiness failure missing retry hint: %#v", rr.Header())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +48,9 @@ func TestReadinessEntersDrainBeforeShutdown(t *testing.T) {
|
|||||||
if draining.Code != http.StatusServiceUnavailable || checks != 1 || strings.Contains(draining.Body.String(), "server is draining") {
|
if draining.Code != http.StatusServiceUnavailable || checks != 1 || strings.Contains(draining.Body.String(), "server is draining") {
|
||||||
t.Fatalf("during drain: status=%d checks=%d body=%s", draining.Code, checks, draining.Body.String())
|
t.Fatalf("during drain: status=%d checks=%d body=%s", draining.Code, checks, draining.Body.String())
|
||||||
}
|
}
|
||||||
|
if draining.Header().Get("Retry-After") != "1" || draining.Header().Get("Cache-Control") != "no-store" {
|
||||||
|
t.Fatalf("draining readiness headers: %#v", draining.Header())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVersionIsPublicAndBounded(t *testing.T) {
|
func TestVersionIsPublicAndBounded(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user