From 4aea7f4910478712953eacb5ec0e9390cf665bc2 Mon Sep 17 00:00:00 2001 From: Fiona Date: Mon, 7 Sep 2026 02:29:51 -0700 Subject: [PATCH 1/2] fix: preserve base URL path prefix when resolving endpoint paths WithBaseURL fed the parsed URL directly to url.URL.ResolveReference, whose RFC 3986 semantics replace the last path segment when the base path lacks a trailing slash: base "https://gateway.example.com/api" resolved "/rum/data/query" to "/rum/data/query" instead of "/api/rum/data/query", leaving deployments that serve the API under a path prefix unreachable. Normalize the base path to end with "/" so relative resolution appends endpoint paths; both newRequestWithAppKey and the multipart upload path resolve against the same BaseURL. --- options.go | 8 ++++++++ options_test.go | 28 +++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/options.go b/options.go index 5f67acf..6b576c6 100644 --- a/options.go +++ b/options.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "net/url" + "strings" "time" ) @@ -11,6 +12,10 @@ import ( type Option func(*Client) // WithBaseURL overrides the API base URL (default https://api.flashcat.cloud). +// A path prefix is preserved: with "https://gateway.example.com/api", requests +// go to /api/. The path is normalized to end with "/" so that +// relative resolution appends endpoint paths instead of replacing the last +// path segment. func WithBaseURL(raw string) Option { parsed, err := url.Parse(raw) return func(c *Client) { @@ -18,6 +23,9 @@ func WithBaseURL(raw string) Option { c.optionErr = fmt.Errorf("flashduty: invalid base URL %q: %w", raw, err) return } + if !strings.HasSuffix(parsed.Path, "/") { + parsed.Path += "/" + } c.BaseURL = parsed } } diff --git a/options_test.go b/options_test.go index 12cdab4..11b895e 100644 --- a/options_test.go +++ b/options_test.go @@ -18,7 +18,7 @@ func TestNewClientDefaultsAndOptions(t *testing.T) { if err != nil { t.Fatal(err) } - if c.BaseURL.String() != "https://example.test" { + if c.BaseURL.String() != "https://example.test/" { t.Fatalf("BaseURL = %s", c.BaseURL) } if c.UserAgent != "ua/1" { @@ -35,6 +35,32 @@ func TestWithBaseURLInvalidReturnsError(t *testing.T) { } } +func TestWithBaseURLPreservesPathPrefix(t *testing.T) { + for _, tc := range []struct { + base string + want string + }{ + {"https://example.test", "https://example.test/rum/data/query"}, + {"https://example.test/", "https://example.test/rum/data/query"}, + {"https://example.test/api", "https://example.test/api/rum/data/query"}, + {"https://example.test/api/", "https://example.test/api/rum/data/query"}, + {"https://example.test/a/b", "https://example.test/a/b/rum/data/query"}, + } { + c, err := NewClient("KEY", WithBaseURL(tc.base)) + if err != nil { + t.Fatalf("NewClient(%q): %v", tc.base, err) + } + req, err := c.newRequest(t.Context(), http.MethodPost, "/rum/data/query", nil) + if err != nil { + t.Fatalf("newRequest with base %q: %v", tc.base, err) + } + req.URL.RawQuery = "" + if got := req.URL.String(); got != tc.want { + t.Errorf("base %q: request URL = %q, want %q", tc.base, got, tc.want) + } + } +} + func TestWithHTTPClientNilIgnored(t *testing.T) { c, err := NewClient("KEY", WithHTTPClient(nil)) if err != nil || c.client == nil { From b59dfcbc5aa654590cadd0e61a71a5e88fc3b14d Mon Sep 17 00:00:00 2001 From: Fiona Date: Mon, 7 Sep 2026 02:52:55 -0700 Subject: [PATCH 2/2] test: cover host:port base URLs with and without a path prefix --- options_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/options_test.go b/options_test.go index 11b895e..1c3af01 100644 --- a/options_test.go +++ b/options_test.go @@ -45,6 +45,8 @@ func TestWithBaseURLPreservesPathPrefix(t *testing.T) { {"https://example.test/api", "https://example.test/api/rum/data/query"}, {"https://example.test/api/", "https://example.test/api/rum/data/query"}, {"https://example.test/a/b", "https://example.test/a/b/rum/data/query"}, + {"http://192.0.2.10:12345", "http://192.0.2.10:12345/rum/data/query"}, + {"http://192.0.2.10:12345/api", "http://192.0.2.10:12345/api/rum/data/query"}, } { c, err := NewClient("KEY", WithBaseURL(tc.base)) if err != nil {