From 0a4d2946842fc6a8a7e189248f2a8345ce01343a Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Fri, 3 Jul 2026 14:53:33 +1000 Subject: [PATCH] feat: redirect / to the web UI The web UI is served under /ui, but hitting the bare domain returned the API's JSON identity blob, so browsers never landed on the app. The root now redirects to /ui/; the identity blob (name + version) moves to /version so monitoring can still read it. - redirect GET / to /ui/ (302) - serve the former root JSON at /version - update the server test to assert the redirect and the /version payload --- internal/server/server.go | 7 +++++++ internal/server/server_test.go | 23 +++++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index 6531606..04059c1 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -95,6 +95,7 @@ func (s *Server) routes() chi.Router { r.Get("/health", s.handleHealth) r.Get("/", s.handleRoot) + r.Get("/version", s.handleVersion) proxyHandler := v1.NewProxyHandler(s.engine, s.virtEngine, s.db, s.store, s.localHandler) r.Mount("/api/v1", proxyHandler.Routes()) @@ -143,7 +144,13 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `{"status":"ok"}`) } +// handleRoot sends browsers landing on the bare domain to the web UI, which is +// served under /ui. The service identity that used to live here is at /version. func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/ui/", http.StatusFound) +} + +func (s *Server) handleVersion(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) fmt.Fprintf(w, `{"name":"artifactapi","version":"%s"}`, s.version) diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 62b7bcf..fa12f6c 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -129,13 +129,32 @@ func req(t *testing.T, method, path string, body string) (*http.Response, []byte return resp, b } +// reqNoRedirect issues a request without following redirects so the response's +// status and Location header can be asserted directly. +func reqNoRedirect(t *testing.T, method, path string) *http.Response { + t.Helper() + rq, _ := http.NewRequest(method, testTS.URL+path, nil) + client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { + return http.ErrUseLastResponse + }} + resp, err := client.Do(rq) + if err != nil { + t.Fatalf("%s %s: %v", method, path, err) + } + resp.Body.Close() + return resp +} + func TestServerHealthAndRoot(t *testing.T) { requireStack(t) if resp, _ := req(t, "GET", "/health", ""); resp.StatusCode != 200 { t.Errorf("health: %d", resp.StatusCode) } - if resp, b := req(t, "GET", "/", ""); resp.StatusCode != 200 || !strings.Contains(string(b), "test-version") { - t.Errorf("root: %d %s", resp.StatusCode, b) + if resp := reqNoRedirect(t, "GET", "/"); resp.StatusCode != http.StatusFound || resp.Header.Get("Location") != "/ui/" { + t.Errorf("root redirect: %d %q", resp.StatusCode, resp.Header.Get("Location")) + } + if resp, b := req(t, "GET", "/version", ""); resp.StatusCode != 200 || !strings.Contains(string(b), "test-version") { + t.Errorf("version: %d %s", resp.StatusCode, b) } if resp, _ := req(t, "GET", "/api/v2/health", ""); resp.StatusCode != 200 { t.Errorf("health v2: %d", resp.StatusCode)