fix(serve): mount /healthz on the public origin
The /healthz route was registered inside httpapi.Server.Routes() but the root mux only attached that handler at /auth/ and /api/, so any request to /healthz fell through to the static file server and got 404'd. Caddy's reverse-proxy and the deploy workflow's curl-based health check both hit the public origin, so the in-container healthcheck reported 'unhealthy' and CI never marked the deploy as verified. Mount /healthz on the root mux explicitly. Add a serve_test.go that asserts the same routing topology so the regression cannot return silently.
This commit is contained in:
@@ -140,6 +140,11 @@ func runServe(args []string) error {
|
|||||||
apiHandler := api.Routes()
|
apiHandler := api.Routes()
|
||||||
root.Handle("/auth/", apiHandler)
|
root.Handle("/auth/", apiHandler)
|
||||||
root.Handle("/api/", apiHandler)
|
root.Handle("/api/", apiHandler)
|
||||||
|
// /healthz is mounted directly so the static fall-through handler
|
||||||
|
// below does not shadow it. The api.Routes() mux registers it for
|
||||||
|
// completeness but with apiHandler attached only at /auth/ and
|
||||||
|
// /api/, the route is otherwise unreachable from the public origin.
|
||||||
|
root.Handle("/healthz", apiHandler)
|
||||||
|
|
||||||
pub, err := fs.Sub(publicFS, "web/public")
|
pub, err := fs.Sub(publicFS, "web/public")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestServeMounts ensures the public origin exposes /healthz, /auth/*,
|
||||||
|
// and /api/* (auth-protected). It uses the same routing topology as
|
||||||
|
// runServe but skips the embedded file system, since the static
|
||||||
|
// fall-through is what shadowed /healthz before this test existed.
|
||||||
|
func TestServeMounts(t *testing.T) {
|
||||||
|
root := http.NewServeMux()
|
||||||
|
apiHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/healthz":
|
||||||
|
_, _ = io.WriteString(w, `{"status":"ok"}`)
|
||||||
|
case "/auth/login":
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
case "/api/whoami":
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
root.Handle("/auth/", apiHandler)
|
||||||
|
root.Handle("/api/", apiHandler)
|
||||||
|
root.Handle("/healthz", apiHandler)
|
||||||
|
root.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}))
|
||||||
|
|
||||||
|
srv := httptest.NewServer(root)
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
path string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"/healthz", http.StatusOK},
|
||||||
|
{"/auth/login", http.StatusMethodNotAllowed},
|
||||||
|
{"/api/whoami", http.StatusUnauthorized},
|
||||||
|
{"/does-not-exist", http.StatusNotFound},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
resp, err := http.Get(srv.URL + tc.path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GET %s: %v", tc.path, err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode != tc.want {
|
||||||
|
t.Errorf("%s: got %d, want %d", tc.path, resp.StatusCode, tc.want)
|
||||||
|
}
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if tc.path == "/healthz" && !strings.Contains(string(body), `"status":"ok"`) {
|
||||||
|
t.Errorf("/healthz body = %q, want it to contain status:ok", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user