Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
45 changes: 35 additions & 10 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.",
})
}
47 changes: 47 additions & 0 deletions api/api_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading