From 423b6d086ec627d6d2eae102cca6189787ac0ec6 Mon Sep 17 00:00:00 2001 From: Zihan Dai Date: Wed, 11 Nov 2026 03:26:06 +1100 Subject: [PATCH] URIBuilder: do not produce '//' when appending to a path that ends with '/' A path that ends with '/' has an empty last segment. appendPath and appendPathSegments added the new segments after it, so new URIBuilder("http://host/api/").appendPath("items") built http://host/api//items, and a base of http://host/ built http://host//items. The empty last segment is now replaced by the appended segments. --- .../org/apache/hc/core5/net/URIBuilder.java | 3 +++ .../apache/hc/core5/net/TestURIBuilder.java | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java b/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java index 218f300a5..44014183b 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java @@ -717,6 +717,9 @@ public URIBuilder appendPathSegments(final List pathSegments) { if (pathSegments != null && !pathSegments.isEmpty()) { if (this.pathSegments == null) { this.pathSegments = new ArrayList<>(); + } else if (!this.pathSegments.isEmpty() && this.pathSegments.get(this.pathSegments.size() - 1).isEmpty()) { + // Path ending with a slash: append after the slash instead of producing '//' + this.pathSegments.remove(this.pathSegments.size() - 1); } this.pathSegments.addAll(pathSegments); this.encodedSchemeSpecificPart = null; diff --git a/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java b/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java index fcef8d125..8fed31883 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java @@ -770,6 +770,26 @@ void testAppendNullSegmentsList() throws Exception { Assertions.assertEquals(URI.create("http://myhost"), uri); } + @Test + void testAppendToPathWithTrailingSlash() throws Exception { + Assertions.assertEquals(URI.create("https://somehost/api"), + new URIBuilder("https://somehost/").appendPath("api").build()); + Assertions.assertEquals(URI.create("https://somehost/api/v1/resources/idA"), + new URIBuilder("https://somehost/api/").appendPath("v1/resources").appendPath("idA").build()); + Assertions.assertEquals(URI.create("https://somehost/api/v1/"), + new URIBuilder("https://somehost").setPath("/api/").appendPath("/v1/").build()); + Assertions.assertEquals(URI.create("https://somehost/api/"), + new URIBuilder("https://somehost/api/").appendPath("/").build()); + } + + @Test + void testAppendSegmentsToPathWithTrailingSlash() throws Exception { + Assertions.assertEquals(URI.create("http://myhost/api/v3/products"), + new URIBuilder("http://myhost/").appendPathSegments("api", "v3", "products").build()); + Assertions.assertEquals(URI.create("http://myhost/api/v3/customers"), + new URIBuilder("http://myhost/api/").appendPathSegments(Arrays.asList("v3", "customers")).build()); + } + @Test void testNoAuthorityAndPathSegments() throws Exception { final URI uri = new URIBuilder()