diff --git a/API.md b/API.md index e4a501f..57811bc 100644 --- a/API.md +++ b/API.md @@ -316,6 +316,24 @@ Simple health check endpoint to verify the API server is running. --- +### Unknown routes + +Any request to a path not listed above (for example `/jit-connect` on an older running instance after the CLI was upgraded) returns: + +- **Status Code:** `426 Upgrade Required` +- **Content-Type:** `application/json` + +```json +{ + "error": "api_route_unavailable", + "message": "This API path is not supported by the running Olm instance. Restart the client after upgrading." +} +``` + +Clients should treat `426` as a stale Olm API daemon and prompt the user to restart the background client (`down` then `up`). Older instances that predate this behavior may still return Go's default `404` for unknown paths. + +--- + ## Usage Examples ### Update metadata before connecting (recommended) diff --git a/api/api.go b/api/api.go index 895140b..ef6074b 100644 --- a/api/api.go +++ b/api/api.go @@ -13,6 +13,16 @@ import ( "github.com/fosrl/newt/network" ) +// StatusAPIServiceOutdated is returned for API paths the running Olm instance does not +// implement. Clients should treat this as a stale daemon that must be restarted after upgrading. +const StatusAPIServiceOutdated = http.StatusUpgradeRequired + +// APIErrorResponse is a machine-readable error payload for API error responses. +type APIErrorResponse struct { + Error string `json:"error"` + Message string `json:"message"` +} + // ConnectionRequest defines the structure for an incoming connection request type ConnectionRequest struct { ID string `json:"id"` @@ -170,16 +180,7 @@ func (s *API) Start() error { } mux := http.NewServeMux() - mux.HandleFunc("/connect", s.handleConnect) - mux.HandleFunc("/status", s.handleStatus) - mux.HandleFunc("/switch-org", s.handleSwitchOrg) - mux.HandleFunc("/metadata", s.handleMetadataChange) - mux.HandleFunc("/disconnect", s.handleDisconnect) - mux.HandleFunc("/exit", s.handleExit) - mux.HandleFunc("/health", s.handleHealth) - mux.HandleFunc("/rebind", s.handleRebind) - mux.HandleFunc("/power-mode", s.handlePowerMode) - mux.HandleFunc("/jit-connect", s.handleJITConnect) + s.registerRoutes(mux) s.server = &http.Server{ Handler: mux, @@ -732,3 +733,27 @@ func (s *API) handlePowerMode(w http.ResponseWriter, r *http.Request) { "status": fmt.Sprintf("power mode changed to %s successfully", req.Mode), }) } + +func (s *API) registerRoutes(mux *http.ServeMux) { + mux.HandleFunc("/connect", s.handleConnect) + mux.HandleFunc("/status", s.handleStatus) + mux.HandleFunc("/switch-org", s.handleSwitchOrg) + mux.HandleFunc("/metadata", s.handleMetadataChange) + mux.HandleFunc("/disconnect", s.handleDisconnect) + mux.HandleFunc("/exit", s.handleExit) + mux.HandleFunc("/health", s.handleHealth) + mux.HandleFunc("/rebind", s.handleRebind) + mux.HandleFunc("/power-mode", s.handlePowerMode) + mux.HandleFunc("/jit-connect", s.handleJITConnect) + mux.HandleFunc("/", s.handleUnknownRoute) +} + +// handleUnknownRoute handles requests to API paths this Olm instance does not implement. +func (s *API) handleUnknownRoute(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(StatusAPIServiceOutdated) + _ = json.NewEncoder(w).Encode(APIErrorResponse{ + Error: "api_route_unavailable", + Message: "This API path is not supported by the running Olm instance. Restart the client after upgrading.", + }) +} diff --git a/api/api_test.go b/api/api_test.go new file mode 100644 index 0000000..6c97eb6 --- /dev/null +++ b/api/api_test.go @@ -0,0 +1,47 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestUnknownRouteReturnsUpgradeRequired(t *testing.T) { + s := NewAPIStub() + mux := http.NewServeMux() + s.registerRoutes(mux) + + req := httptest.NewRequest(http.MethodPost, "/future-endpoint", nil) + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + + if rec.Code != StatusAPIServiceOutdated { + t.Fatalf("status = %d, want %d", rec.Code, StatusAPIServiceOutdated) + } + + var body APIErrorResponse + if err := json.NewDecoder(rec.Body).Decode(&body); err != nil { + t.Fatalf("decode body: %v", err) + } + if body.Error != "api_route_unavailable" { + t.Fatalf("error = %q, want api_route_unavailable", body.Error) + } + if rec.Header().Get("Content-Type") != "application/json" { + t.Fatalf("content-type = %q, want application/json", rec.Header().Get("Content-Type")) + } +} + +func TestKnownRouteNotHandledByCatchAll(t *testing.T) { + s := NewAPIStub() + mux := http.NewServeMux() + s.registerRoutes(mux) + + req := httptest.NewRequest(http.MethodGet, "/health", nil) + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) + } +}