diff --git a/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestInvocationHandler.java b/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestInvocationHandler.java index 5df10f7e18..de77035139 100644 --- a/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestInvocationHandler.java +++ b/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestInvocationHandler.java @@ -157,7 +157,18 @@ private Object executeRequest(final ResourceMethod rm, pathSegments.add(pathTemplate.getSegment()); } } - uriBuilder.appendPathSegments(pathSegments); + if (!pathSegments.isEmpty()) { + // A base URI ending in '/' (as many are written) has an empty last path + // segment. Appending after it would produce '//'; drop it first, exactly + // as ResourceIfaceScanner#join already does when joining a resource class's + // path with its method's path. + final List baseSegments = uriBuilder.getPathSegments(); + if (!baseSegments.isEmpty() && baseSegments.get(baseSegments.size() - 1).isEmpty()) { + baseSegments.remove(baseSegments.size() - 1); + } + baseSegments.addAll(pathSegments); + uriBuilder.setPathSegments(baseSegments); + } uriBuilder.addParameters(queryParams); requestUri = uriBuilder.build(); } catch (final URISyntaxException ex) { diff --git a/httpclient5-jakarta-rest-client/src/test/java/org/apache/hc/client5/http/rest/RestClientIntegrationTest.java b/httpclient5-jakarta-rest-client/src/test/java/org/apache/hc/client5/http/rest/RestClientIntegrationTest.java index b1a1037b00..c7af1f9855 100644 --- a/httpclient5-jakarta-rest-client/src/test/java/org/apache/hc/client5/http/rest/RestClientIntegrationTest.java +++ b/httpclient5-jakarta-rest-client/src/test/java/org/apache/hc/client5/http/rest/RestClientIntegrationTest.java @@ -160,6 +160,24 @@ void readsFormUrlEncodedResponseAsNameValuePairs() throws Exception { assertThat(pairs.get(1).getValue()).isEqualTo("en"); } + @Test + void getBook_withTrailingSlashBaseUri_doesNotProduceDoubleSlash() throws Exception { + // A base URI ending in '/', as many are written, must not combine with the resource + // path to produce '//books/...': the server only has a context registered at + // "/books", so a leading double slash would not match it and the call would fail. + final URI trailingSlashBaseUri = new URI(baseUri.toString() + "/"); + final BookResource client = RestClientBuilder.newBuilder() + .baseUri(trailingSlashBaseUri) + .httpClient(httpClient) + .build(BookResource.class); + + final Book fetched = client.getBook("abc-123", "en", "token-1"); + + assertThat(fetched).isNotNull(); + assertThat(fetched.id).isEqualTo("abc-123"); + assertThat(fetched.lang).isEqualTo("en"); + } + @Test void rejectsNonFormResponseForNameValuePairReturnType() { final BookResource client = RestClientBuilder.newBuilder()