Skip to content

Commit 5286530

Browse files
fredbiclaude
andauthored
fix(client): preserve trailing slash on bare-root path pattern (#441)
The reinstateSlash logic in resolveURLPath (introduced in 6cdcb95 to fix #289) deliberately excluded pathPattern == "/" to avoid producing "//" when basePath was empty or "/". That left one combination inconsistent with every other trailing-slash case: basePath=/myservice with pathPattern=/ produced /myservice instead of /myservice/. Replace the pattern-shape carve-out with a HasSuffix check on the already-built urlPath, which is idempotent and keeps the double-slash cases unchanged. Closes #101 Signed-off-by: Frédéric BIDON <fredbi@yahoo.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0d73e8c commit 5286530

2 files changed

Lines changed: 43 additions & 3 deletions

File tree

client/internal/request/request.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -533,14 +533,18 @@ func (r *Request) resolveURLPath(basePath string) (string, url.Values, error) {
533533
}
534534
}
535535

536-
reinstateSlash := pathPatternURL.Path != "" && pathPatternURL.Path != "/" &&
537-
pathPatternURL.Path[len(pathPatternURL.Path)-1] == '/'
536+
// path.Join strips trailing slashes; reinstate one whenever the
537+
// pathPattern carried it, including the bare-root case ("/" under a
538+
// non-empty basePath, which path.Join would collapse to "/basepath").
539+
// The HasSuffix check on urlPath keeps the rewrite idempotent and
540+
// avoids producing "//" when basePath is "/" or empty.
541+
reinstateSlash := strings.HasSuffix(pathPatternURL.Path, "/")
538542

539543
urlPath := path.Join(basePathURL.Path, pathPatternURL.Path)
540544
for k, v := range r.pathParams {
541545
urlPath = strings.ReplaceAll(urlPath, "{"+k+"}", url.PathEscape(v))
542546
}
543-
if reinstateSlash {
547+
if reinstateSlash && !strings.HasSuffix(urlPath, "/") {
544548
urlPath += "/"
545549
}
546550

client/internal/request/request_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,42 @@ func TestBuildRequest_BuildHTTP_EscapedPath(t *testing.T) {
681681
assert.EqualT(t, req.URL.RawPath, req.URL.EscapedPath())
682682
}
683683

684+
// TestBuildRequest_BuildHTTP_RootPathTrailingSlash locks in the fix for
685+
// issue #101: the bare-root pattern "/" under a non-empty basePath must
686+
// keep its trailing slash, and the bare-root cases that the pre-fix
687+
// formula avoided ("" / "/" basePath) must still not produce "//".
688+
func TestBuildRequest_BuildHTTP_RootPathTrailingSlash(t *testing.T) {
689+
const bp = "/basepath"
690+
cases := []struct {
691+
name string
692+
basePath string
693+
pathPattern string
694+
wantPath string
695+
}{
696+
{"root pattern under non-empty basePath keeps slash (#101)", bp, "/", bp + "/"},
697+
{"root pattern under '/' basePath stays '/'", "/", "/", "/"},
698+
{"root pattern under empty basePath stays '/'", "", "/", "/"},
699+
{"non-root trailing slash still preserved", bp, "/users/", bp + "/users/"},
700+
{"no trailing slash on pattern produces no trailing slash", bp, "/users", bp + "/users"},
701+
}
702+
703+
for _, tc := range cases {
704+
t.Run(tc.name, func(t *testing.T) {
705+
reqWrtr := runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error {
706+
return nil
707+
})
708+
r := New(http.MethodGet, tc.pathPattern, reqWrtr)
709+
710+
req, cancel, err := r.BuildHTTPContext(t.Context(), runtime.JSONMime, tc.basePath, testProducers, nil, nil)
711+
require.NoError(t, err)
712+
t.Cleanup(cancel)
713+
require.NotNil(t, req)
714+
715+
assert.EqualT(t, tc.wantPath, req.URL.Path)
716+
})
717+
}
718+
}
719+
684720
func TestBuildRequest_BuildHTTP_BasePathWithQueryParameters(t *testing.T) {
685721
reqWrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error {
686722
_ = req.SetBodyParam(nil)

0 commit comments

Comments
 (0)