Skip to content

feat(server): add /healthz endpoint for container health checks#3060

Open
saivedant169 wants to merge 2 commits intogoogleapis:mainfrom
saivedant169:feat/healthz-endpoint
Open

feat(server): add /healthz endpoint for container health checks#3060
saivedant169 wants to merge 2 commits intogoogleapis:mainfrom
saivedant169:feat/healthz-endpoint

Conversation

@saivedant169
Copy link
Copy Markdown

Fixes #2644

Summary

Adds a /healthz endpoint to the Toolbox HTTP server so container orchestrators (Kubernetes liveness/readiness probes, Docker HEALTHCHECK, Cloud Run startup probes) have a dedicated, lightweight path to hit. The response is HTTP 200 with 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

  • Registered r.Get("/healthz", ...) in internal/server/server.go right after the default / handler, so it inherits the same CORS and host-check middleware already applied at the router level.
  • Returns Content-Type: application/json with body {"status":"ok"}.
  • No new dependencies.

Testing

Added TestHealthz in internal/server/server_test.go. It follows the same pattern as TestServe: spins up a real server on a free port, sends a GET to /healthz, and verifies the status code, the Content-Type header, and the JSON body. Runs on port 5004 to avoid collisions with other tests in the package.

$ go test ./internal/server/ -run "TestServe|TestHealthz" -count=1
ok  	github.com/googleapis/mcp-toolbox/internal/server	1.283s

$ go test ./internal/server/ -count=1
ok  	github.com/googleapis/mcp-toolbox/internal/server	2.220s

Also verified go vet ./internal/server/... and gofmt -l are clean.

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
@saivedant169 saivedant169 requested a review from a team as a code owner April 15, 2026 02:31
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread internal/server/server.go
// (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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Comment on lines +165 to +171
go func() {
defer close(errCh)
err = s.Serve(ctx)
if err != nil {
errCh <- err
}
}()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.
@saivedant169
Copy link
Copy Markdown
Author

saivedant169 commented Apr 15, 2026

Pushed a fix for both items in commit 27d8d45.

  1. /healthz and the host check: I went with the in-middleware bypass rather than a separate router group. The handler now short-circuits the host validation only for /healthz, so DNS rebinding protection still applies to every other path. A new TestHealthzBypassesHostCheck confirms /healthz returns 200 and / returns 403 when AllowedHosts does not include the request host.

  2. Race on err: replaced the goroutine's reassignment with a shadowed serveErr local. Verified clean with go test -race.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add /healthz endpoint or healthcheck CLI command for Docker health checks

2 participants