From 6c07900d0d955a6a1db5b2ab770d5a2f641fb25f Mon Sep 17 00:00:00 2001 From: Jonathan Lam Date: Tue, 17 Mar 2026 20:09:12 -0700 Subject: [PATCH] fix: prepend api. prefix in NullifyClient and use POST for metrics overview NullifyClient and refreshingAuthTransport were constructing BaseURL without the api. prefix, and authTransport was overriding the request host back to the bare form. The status command was also using GET instead of POST for /admin/metrics/overview. --- cmd/cli/cmd/status.go | 4 +++- internal/client/client.go | 7 ++++++- internal/client/refreshing_transport.go | 11 +++++++---- internal/client/transport.go | 3 --- internal/lib/http.go | 26 +++++++++++++++++++++++++ 5 files changed, 42 insertions(+), 9 deletions(-) diff --git a/cmd/cli/cmd/status.go b/cmd/cli/cmd/status.go index 1ed3912..55f1277 100644 --- a/cmd/cli/cmd/status.go +++ b/cmd/cli/cmd/status.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "strings" "github.com/nullify-platform/cli/internal/auth" "github.com/nullify-platform/cli/internal/client" @@ -42,7 +43,8 @@ var securityStatusCmd = &cobra.Command{ // Fetch metrics overview qs := lib.BuildQueryString(queryParams) - overviewBody, err := lib.DoGet(ctx, nullifyClient.HttpClient, nullifyClient.BaseURL, "/admin/metrics/overview"+qs) + overviewBody, err := lib.DoPostJSON(ctx, nullifyClient.HttpClient, nullifyClient.BaseURL, "/admin/metrics/overview"+qs, strings.NewReader(`{"query":{}}`)) + if err != nil { fmt.Fprintf(os.Stderr, "Error fetching metrics: %v\n", err) os.Exit(ExitNetworkError) diff --git a/internal/client/client.go b/internal/client/client.go index 93b0a5c..b069153 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -26,6 +26,11 @@ type NullifyClient struct { // NewNullifyClient creates a client for the given host with bearer token auth. func NewNullifyClient(nullifyHost string, token string) *NullifyClient { + apiHost := nullifyHost + if !strings.HasPrefix(nullifyHost, "api.") { + apiHost = "api." + nullifyHost + } + httpClient := &http.Client{ Timeout: 30 * time.Second, Transport: NewRetryTransport(&authTransport{ @@ -37,7 +42,7 @@ func NewNullifyClient(nullifyHost string, token string) *NullifyClient { return &NullifyClient{ Host: nullifyHost, - BaseURL: "https://" + nullifyHost, + BaseURL: "https://" + apiHost, Token: token, HttpClient: httpClient, } diff --git a/internal/client/refreshing_transport.go b/internal/client/refreshing_transport.go index 1441c73..f6de03b 100644 --- a/internal/client/refreshing_transport.go +++ b/internal/client/refreshing_transport.go @@ -3,6 +3,7 @@ package client import ( "context" "net/http" + "strings" "sync" "time" @@ -47,9 +48,14 @@ func NewRefreshingNullifyClient(nullifyHost string, tokenProvider TokenProvider) Transport: NewRetryTransport(t), } + apiHost := nullifyHost + if !strings.HasPrefix(nullifyHost, "api.") { + apiHost = "api." + nullifyHost + } + return &NullifyClient{ Host: nullifyHost, - BaseURL: "https://" + nullifyHost, + BaseURL: "https://" + apiHost, Token: "", // Token is managed by the refreshing transport; do not use this field directly. HttpClient: httpClient, }, nil @@ -87,9 +93,6 @@ func (t *refreshingAuthTransport) RoundTrip(req *http.Request) (*http.Response, token := t.getToken(req.Context()) r := req.Clone(req.Context()) - r.URL.Scheme = "https" - r.URL.Host = t.nullifyHost - r.Host = t.nullifyHost r.Header.Set("Authorization", "Bearer "+token) r.Header.Set("User-Agent", "Nullify-CLI/mcp") return t.transport.RoundTrip(r) diff --git a/internal/client/transport.go b/internal/client/transport.go index f47f089..3b04d74 100644 --- a/internal/client/transport.go +++ b/internal/client/transport.go @@ -14,9 +14,6 @@ type authTransport struct { func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) { r := req.Clone(req.Context()) - r.URL.Scheme = "https" - r.URL.Host = t.nullifyHost - r.Host = t.nullifyHost r.Header.Set("Authorization", "Bearer "+t.token) r.Header.Set("User-Agent", "Nullify-CLI/"+logger.Version) return t.transport.RoundTrip(r) diff --git a/internal/lib/http.go b/internal/lib/http.go index d67d387..a318ee5 100644 --- a/internal/lib/http.go +++ b/internal/lib/http.go @@ -59,6 +59,32 @@ func DoPost(ctx context.Context, httpClient Doer, baseURL, path string) (string, return string(body), nil } +// DoPostJSON performs a POST request with a JSON body and returns the response body as a string. +func DoPostJSON(ctx context.Context, httpClient Doer, baseURL, path string, body io.Reader) (string, error) { + req, err := http.NewRequestWithContext(ctx, "POST", baseURL+path, body) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := httpClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20)) + if err != nil { + return "", err + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return "", fmt.Errorf("API returned %d: %s", resp.StatusCode, string(respBody)) + } + + return string(respBody), nil +} + // DoGet performs a GET request and returns the response body as a string. // Returns an error if the request fails or the status code is not 2xx. func DoGet(ctx context.Context, httpClient Doer, baseURL, path string) (string, error) {