From d1117f853763b23d0fdf24daeff99b77dcc74e61 Mon Sep 17 00:00:00 2001 From: Vladimir Saraikin Date: Mon, 13 Jul 2026 17:29:35 +0200 Subject: [PATCH] fix: joinUrl no longer collapses the URL scheme joinUrl used path.Join, whose path.Clean collapses the // in a URL scheme, so {{joinUrl "http://localhost" "p1" "p2"}} produced http:/localhost/p1/p2 instead of the documented http://localhost/p1/p2. Use net/url.JoinPath, which preserves the scheme. --- internal/templater/funcs.go | 11 ++++++++--- internal/templater/funcs_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 internal/templater/funcs_test.go diff --git a/internal/templater/funcs.go b/internal/templater/funcs.go index 2fe85b96b0..0ffd233dc0 100644 --- a/internal/templater/funcs.go +++ b/internal/templater/funcs.go @@ -3,8 +3,8 @@ package templater import ( "maps" "math/rand/v2" + "net/url" "os" - "path" "path/filepath" "runtime" "strings" @@ -103,8 +103,13 @@ func joinEnv(elem ...string) string { return strings.Join(elem, string(os.PathListSeparator)) } -func joinUrl(elem ...string) string { - return path.Join(elem...) +func joinUrl(elem ...string) (string, error) { + if len(elem) == 0 { + return "", nil + } + // Use net/url.JoinPath rather than path.Join: the latter runs path.Clean, + // which collapses the "//" in a URL scheme (e.g. "http://" -> "http:/"). + return url.JoinPath(elem[0], elem[1:]...) } func merge(base map[string]any, v ...map[string]any) map[string]any { diff --git a/internal/templater/funcs_test.go b/internal/templater/funcs_test.go new file mode 100644 index 0000000000..62c288b702 --- /dev/null +++ b/internal/templater/funcs_test.go @@ -0,0 +1,27 @@ +package templater + +import "testing" + +func TestJoinUrl(t *testing.T) { + for _, tt := range []struct { + name string + elem []string + want string + }{ + // The URL scheme's "//" must be preserved, not collapsed by path.Clean. + {"scheme preserved", []string{"http://localhost", "path1", "path2"}, "http://localhost/path1/path2"}, + {"https with base path", []string{"https://example.com/api", "v1", "users"}, "https://example.com/api/v1/users"}, + {"trailing slash on base", []string{"http://localhost/", "path"}, "http://localhost/path"}, + {"single element", []string{"http://localhost"}, "http://localhost/"}, + } { + t.Run(tt.name, func(t *testing.T) { + got, err := joinUrl(tt.elem...) + if err != nil { + t.Fatalf("joinUrl(%q) unexpected error: %v", tt.elem, err) + } + if got != tt.want { + t.Errorf("joinUrl(%q) = %q; want %q", tt.elem, got, tt.want) + } + }) + } +}