feat(server): add /healthz endpoint for container health checks#3060
feat(server): add /healthz endpoint for container health checks#3060saivedant169 wants to merge 2 commits intogoogleapis:mainfrom
Conversation
Adds a lightweight /healthz endpoint that returns 200 OK with a JSON
body of {"status":"ok"}. Container orchestrators like Kubernetes and
Docker can hit this for liveness or readiness probes without having
to rely on the main "/" handler, which is intended as a landing page.
Added TestHealthz covering the status code, Content-Type, and JSON
payload. Runs alongside the existing TestServe on a distinct port so
it does not conflict with other server tests in the package.
Fixes googleapis#2644
There was a problem hiding this comment.
Code Review
This pull request introduces a new /healthz endpoint to the server for container orchestration health checks, along with a corresponding unit test. Feedback was provided regarding the placement of the endpoint on the main router, which may lead to 403 Forbidden errors if host-header validation is enabled and probes use IP addresses. Additionally, a race condition was identified in the test code where the error variable is shared across goroutines.
| // (Kubernetes liveness/readiness probes, Docker HEALTHCHECK, etc.). | ||
| // Returns 200 OK with a small JSON body so probes can rely on both | ||
| // status code and payload. | ||
| r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) { |
There was a problem hiding this comment.
The /healthz endpoint is registered on the main router, which is subject to the hostCheck middleware applied at line 417. In many containerized environments (like Kubernetes), health probes hit the endpoint using the Pod's IP address or localhost. If AllowedHosts is configured with specific domains, these probes will fail with a 403 Forbidden error, potentially causing deployment failures or restart loops. Consider registering health check endpoints on a router group that bypasses host header validation.
| go func() { | ||
| defer close(errCh) | ||
| err = s.Serve(ctx) | ||
| if err != nil { | ||
| errCh <- err | ||
| } | ||
| }() |
There was a problem hiding this comment.
There is a race condition in this test. The goroutine captures and modifies the err variable from the outer scope (declared at line 154), while the main goroutine reassigns the same variable at line 174 (resp, err := http.Get(url)). You should use a local variable within the goroutine to avoid this race.
go func() {
defer close(errCh)
if err := s.Serve(ctx); err != nil {
errCh <- err
}
}()Addresses Gemini Code Assist review on PR googleapis#3060. 1. Health probes hit /healthz via the pod IP or localhost in Kubernetes, Docker, and Cloud Run. With a strict AllowedHosts config, that previously returned 403 and broke liveness probes. The hostCheck middleware now skips validation for /healthz only, keeping DNS rebinding protection in place for every other path. 2. The TestHealthz goroutine captured the outer err variable while the main goroutine reassigned it on the http.Get call. Replaced with a shadowed serveErr local so the goroutine no longer races. Added TestHealthzBypassesHostCheck which sets AllowedHosts to a non-matching host, then asserts /healthz returns 200 and / returns 403. Confirms the bypass works without weakening host validation elsewhere. Tests pass with -race.
|
Pushed a fix for both items in commit 27d8d45.
|
Fixes #2644
Summary
Adds a
/healthzendpoint to the Toolbox HTTP server so container orchestrators (Kubernetes liveness/readiness probes, DockerHEALTHCHECK, Cloud Run startup probes) have a dedicated, lightweight path to hit. The response isHTTP 200with a JSON body of{"status":"ok"}, so probes can check either the status code or the payload depending on their configuration.Why a separate endpoint
The existing
/handler is a landing page that returns a greeting string. Reusing it for health checks is fine today but couples probe behavior to a user-facing route, and the non-JSON body makes it awkward for tooling that parses health responses. Giving probes their own path follows the convention most Go services already use and keeps/free to evolve as a human-facing entry point.Implementation
r.Get("/healthz", ...)ininternal/server/server.goright after the default/handler, so it inherits the same CORS and host-check middleware already applied at the router level.Content-Type: application/jsonwith body{"status":"ok"}.Testing
Added
TestHealthzininternal/server/server_test.go. It follows the same pattern asTestServe: spins up a real server on a free port, sends a GET to/healthz, and verifies the status code, theContent-Typeheader, and the JSON body. Runs on port5004to avoid collisions with other tests in the package.Also verified
go vet ./internal/server/...andgofmt -lare clean.