diff --git a/spring-web/src/main/java/org/springframework/web/util/RfcUriParser.java b/spring-web/src/main/java/org/springframework/web/util/RfcUriParser.java index 3198e93590a..b7c4e824200 100644 --- a/spring-web/src/main/java/org/springframework/web/util/RfcUriParser.java +++ b/spring-web/src/main/java/org/springframework/web/util/RfcUriParser.java @@ -24,6 +24,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.core.log.LogDelegateFactory; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Parser for URIs based on RFC 3986 syntax. @@ -545,7 +546,8 @@ abstract class RfcUriParser { public InternalParser capturePort() { verify(this.openCurlyBracketCount == 0, this, "Bad authority"); - this.port = captureComponent("port"); + String value = captureComponent("port"); + this.port = (StringUtils.hasText(value) ? value : null); return this; } diff --git a/spring-web/src/main/java/org/springframework/web/util/UriBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriBuilder.java index 4b033027bbb..14d61406fa4 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriBuilder.java @@ -73,7 +73,7 @@ public interface UriBuilder { /** * Set the URI port. Use this method only when the port needs to be * parameterized with a URI variable. Otherwise use {@link #port(int)}. - * Passing {@code null} will clear the port of this builder. + * Passing {@code null} or an empty String will clear the port of this builder. * @param port the URI port */ UriBuilder port(@Nullable String port); diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index 62eae9fe2bf..a454d3a2cd1 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -513,7 +513,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { @Override public UriComponentsBuilder port(@Nullable String port) { - this.port = port; + this.port = (StringUtils.hasText(port) ? port : null); if (port != null) { resetSchemeSpecificPart(); } diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java index fc0a40a54c3..9222ca41812 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java @@ -937,11 +937,17 @@ class UriComponentsBuilderTests { @ParameterizedTest @EnumSource void verifyInvalidPort(ParserType parserType) { - String url = "http://localhost:XXX/path"; - assertThatIllegalArgumentException() - .isThrownBy(() -> UriComponentsBuilder.fromUriString(url, parserType).build().toUri()); - assertThatIllegalArgumentException() - .isThrownBy(() -> UriComponentsBuilder.fromUriString(url, parserType).build().toUri()); + assertThatIllegalArgumentException().isThrownBy(() -> + UriComponentsBuilder.fromUriString("http://localhost:XXX/path", parserType).build().toUri()); + } + + @ParameterizedTest // gh-37117 + @EnumSource + void verifyEmptyPort(ParserType parserType) { + URI uri = UriComponentsBuilder.fromUriString("http://localhost:/path", parserType).build().toUri(); + assertThat(uri.getHost()).isEqualTo("localhost"); + assertThat(uri.getPort()).isEqualTo(-1); + assertThat(uri.getPath()).isEqualTo("/path"); } @ParameterizedTest // gh-27039 diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsTests.java index ffa6a975d22..43a03ef6e11 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsTests.java @@ -201,10 +201,9 @@ class UriComponentsTests { .isThrownBy(() -> fromUriString("https://example.com:XXX/bar", parserType)); } - @ParameterizedTest - @EnumSource - void invalidPortSet(ParserType parserType) { - UriComponents uriComponents = fromUriString("https://example.com/bar", parserType).port("XXX").build(); + @Test + void invalidPortSet() { + UriComponents uriComponents = fromUriString("https://example.com/bar").port("XXX").build(); assertThatIllegalStateException() .isThrownBy(uriComponents::getPort) @@ -215,6 +214,12 @@ class UriComponentsTests { .withMessage("The port must be an integer: XXX"); } + @Test // gh-37117 + void emptyPortSet() { + UriComponents uriComponents = fromUriString("https://example.com/bar").port("").build(); + assertThat(uriComponents.getPort()).isEqualTo(-1); + } + @Test void expandEncoded() { assertThatIllegalStateException().isThrownBy(() ->