From a1dc6f7d6dc0ac8328c88699e8bf61ced4c3cc45 Mon Sep 17 00:00:00 2001 From: Dominik Madarasz Date: Fri, 30 Oct 2026 08:37:00 +0100 Subject: [PATCH] fix: accept the API address without /api/v2 NEXRENDER_API_URL, --server and profile servers had to spell out the /api/v2 base. Given only the API's address (https://api.nexrender.com, as people naturally write it), every request went to /jobs, /templates and so on at the root and failed with "Not Found", which auth status reported as a rejected credential. A server that is just the address, or ends in /api, now gets /api/v2. Any other path is kept, so gateways that mount the API elsewhere still work, and URLs that already end in /api/v2 are unchanged. Found by Vanguard. --- internal/client/client.go | 2 +- internal/config/config.go | 16 +++++++++++++++- internal/config/config_test.go | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/internal/client/client.go b/internal/client/client.go index 9ec4055..964b2d5 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -23,7 +23,7 @@ func New(server, token string, timeout time.Duration) (*API, error) { server = strings.TrimRight(server, "/") parsed, err := url.Parse(server) if err != nil || parsed.Scheme == "" || parsed.Host == "" { - return nil, clierr.Validation("invalid API server URL", "Use a full URL such as https://api.nexrender.com/api/v2.") + return nil, clierr.Validation("invalid API server URL", "Use the API address, such as https://api.nexrender.com.") } httpClient := &http.Client{Timeout: timeout} editor := func(_ context.Context, req *http.Request) error { diff --git a/internal/config/config.go b/internal/config/config.go index 93f4def..ff80d7f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "net/url" "os" "path/filepath" "strings" @@ -108,6 +109,19 @@ func ValidateProfileName(name string) error { return nil } +// NormalizeServer trims a server URL and, when it is only the API's address +// (https://api.nexrender.com) or ends in /api, adds the /api/v2 base the client needs. +// Other paths are kept as given, for gateways and proxies that mount the API elsewhere. func NormalizeServer(server string) string { - return strings.TrimRight(strings.TrimSpace(server), "/") + server = strings.TrimRight(strings.TrimSpace(server), "/") + parsed, err := url.Parse(server) + if err != nil || parsed.Scheme == "" || parsed.Host == "" { + return server + } + switch parsed.Path { + case "", "/api": + parsed.Path = "/api/v2" + return parsed.String() + } + return server } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 491cb16..d6bd486 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -43,3 +43,21 @@ func TestValidateProfileName(t *testing.T) { } } } + +func TestNormalizeServerDerivesTheAPIBase(t *testing.T) { + cases := map[string]string{ + "https://api.nexrender.com": "https://api.nexrender.com/api/v2", + " https://staging.api.nexrender.com/ ": "https://staging.api.nexrender.com/api/v2", + "https://staging.api.nexrender.com/api": "https://staging.api.nexrender.com/api/v2", + "https://staging.api.nexrender.com/api/v2": "https://staging.api.nexrender.com/api/v2", + "http://127.0.0.1:3000": "http://127.0.0.1:3000/api/v2", + "https://gateway.example.test/nexrender/v2": "https://gateway.example.test/nexrender/v2", + "": "", + "not a url": "not a url", + } + for input, want := range cases { + if got := NormalizeServer(input); got != want { + t.Errorf("NormalizeServer(%q) = %q, want %q", input, got, want) + } + } +}