From 5c747a446235ce9e36969d3eaa80fceb5f2b4521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EB=8F=99=EC=9C=A4=20=28Park=20Dong-Yun=29?= Date: Wed, 8 Apr 2026 03:58:53 +0900 Subject: [PATCH 1/4] Avoid redundant URI object creation in WebClientUtils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prior to this commit, WebClientUtils.getRequestDescription() created a new URI object on every invocation. Since the URI constructor includes validation and parsing, which is already performed by the parameter URI object, this was unnecessarily expensive for a logging utility. This commit reuses the original URI's string representation to avoid redundant parsing. Closes gh-36605 Signed-off-by: 박동윤 (Park Dong-Yun) --- .../function/client/WebClientUtils.java | 29 +++- .../function/client/WebClientUtilsTests.java | 127 ++++++++++++++++++ 2 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientUtilsTests.java diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java index 09765c3f72fb..a9cba89bff77 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java @@ -17,7 +17,6 @@ package org.springframework.web.reactive.function.client; import java.net.URI; -import java.net.URISyntaxException; import java.util.List; import java.util.function.Predicate; @@ -28,7 +27,6 @@ import org.springframework.core.codec.CodecException; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; -import org.springframework.util.StringUtils; /** * Internal methods shared between {@link DefaultWebClient} and @@ -69,16 +67,33 @@ public static Mono>> mapToEntityList(ClientResponse r } /** - * Return a String representation of the request details for logging purposes. + * Return a String representation of the request details for logging purposes + * in "METHOD URI" format. * @since 6.0.16 */ public static String getRequestDescription(HttpMethod httpMethod, URI uri) { - if (StringUtils.hasText(uri.getQuery())) { - try { - uri = new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), null, null); + if (uri.getRawQuery() != null) { + StringBuilder sb = new StringBuilder(); + if (uri.getScheme() != null) { + sb.append(uri.getScheme()).append(':'); } - catch (URISyntaxException ignored) { + if (uri.getHost() != null) { + sb.append("//"); + String host = uri.getHost(); + if (host.indexOf(':') >= 0 && !host.startsWith("[") && !host.endsWith("]")) { + sb.append('[').append(host).append(']'); + } + else { + sb.append(host); + } + if (uri.getPort() != -1) { + sb.append(':').append(uri.getPort()); + } } + if (uri.getPath() != null) { + sb.append(uri.getPath()); + } + return httpMethod.name() + " " + sb; } return httpMethod.name() + " " + uri; } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientUtilsTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientUtilsTests.java new file mode 100644 index 000000000000..eefa0a69bce0 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientUtilsTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.function.client; + +import java.net.URI; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpMethod; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WebClientUtils#getRequestDescription}. + * + * @author Park Dong-Yun + */ +class WebClientUtilsTests { + + @Test + void stripsQueryParams() { + URI uri = URI.create("https://api.example.com/search?q=test&page=1"); + assertThat(WebClientUtils.getRequestDescription(HttpMethod.GET, uri)) + .isEqualTo("GET https://api.example.com/search"); + } + + @Test + void noQueryReturnsAsIs() { + URI uri = URI.create("https://api.example.com/health"); + assertThat(WebClientUtils.getRequestDescription(HttpMethod.GET, uri)) + .isEqualTo("GET https://api.example.com/health"); + } + + @Test + void preservesPort() { + URI uri = URI.create("https://api.example.com:8443/path?q=1"); + assertThat(WebClientUtils.getRequestDescription(HttpMethod.GET, uri)) + .isEqualTo("GET https://api.example.com:8443/path"); + } + + @Test + void decodesPathWhenStrippingQuery() { + URI uri = URI.create("https://host/hello%20world?q=1"); + assertThat(WebClientUtils.getRequestDescription(HttpMethod.GET, uri)) + .isEqualTo("GET https://host/hello world"); + } + + @Test + void stripsUserInfoWithQuery() { + URI uri = URI.create("https://admin:secret@host/api?token=abc"); + assertThat(WebClientUtils.getRequestDescription(HttpMethod.GET, uri)) + .isEqualTo("GET https://host/api"); + } + + @Test + void preservesUserInfoWithoutQuery() { + URI uri = URI.create("https://admin:secret@host/api"); + assertThat(WebClientUtils.getRequestDescription(HttpMethod.GET, uri)) + .isEqualTo("GET https://admin:secret@host/api"); + } + + @Test + void stripsFragmentWithQuery() { + URI uri = URI.create("https://host/page?q=1#section"); + assertThat(WebClientUtils.getRequestDescription(HttpMethod.GET, uri)) + .isEqualTo("GET https://host/page"); + } + + @Test + void preservesFragmentWithoutQuery() { + URI uri = URI.create("https://host/page#section"); + assertThat(WebClientUtils.getRequestDescription(HttpMethod.GET, uri)) + .isEqualTo("GET https://host/page#section"); + } + + @Test + void questionMarkInFragmentNotTreatedAsQuery() { + URI uri = URI.create("https://host/page#frag?param=value"); + assertThat(WebClientUtils.getRequestDescription(HttpMethod.GET, uri)) + .isEqualTo("GET https://host/page#frag?param=value"); + } + + @Test + void ipv6Host() { + URI uri = URI.create("http://[::1]:8080/path?q=1"); + assertThat(WebClientUtils.getRequestDescription(HttpMethod.GET, uri)) + .isEqualTo("GET http://[::1]:8080/path"); + } + + @Test + void opaqueUriUnchanged() { + URI uri = URI.create("mailto:user@example.com?subject=hello"); + assertThat(WebClientUtils.getRequestDescription(HttpMethod.GET, uri)) + .isEqualTo("GET mailto:user@example.com?subject=hello"); + } + + @Test + void relativeUri() { + URI uri = URI.create("/api/search?q=test"); + assertThat(WebClientUtils.getRequestDescription(HttpMethod.GET, uri)) + .isEqualTo("GET /api/search"); + } + + @Test + void prefixesHttpMethod() { + URI uri = URI.create("https://host/resource?v=1"); + assertThat(WebClientUtils.getRequestDescription(HttpMethod.POST, uri)) + .startsWith("POST "); + assertThat(WebClientUtils.getRequestDescription(HttpMethod.DELETE, uri)) + .startsWith("DELETE "); + } + +} From 3d21e39e87726c615dbf8bdbfbf2900ecd6b0576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EB=8F=99=EC=9C=A4=20=28Park=20Dong-Yun=29?= Date: Fri, 10 Apr 2026 23:10:22 +0900 Subject: [PATCH 2/4] Refactor getRequestDescription with early return in WebClientUtils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduce nesting in query-string handling by returning early when rawQuery is null, improving readability without changing behavior. Signed-off-by: 박동윤 (Park Dong-Yun) --- .../function/client/WebClientUtils.java | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java index a9cba89bff77..05dff9d347ae 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java @@ -72,30 +72,32 @@ public static Mono>> mapToEntityList(ClientResponse r * @since 6.0.16 */ public static String getRequestDescription(HttpMethod httpMethod, URI uri) { - if (uri.getRawQuery() != null) { - StringBuilder sb = new StringBuilder(); - if (uri.getScheme() != null) { - sb.append(uri.getScheme()).append(':'); + if (uri.getRawQuery() == null) { + return httpMethod.name() + " " + uri; + } + StringBuilder sb = new StringBuilder(); + if (uri.getScheme() != null) { + sb.append(uri.getScheme()).append(':'); + } + if (uri.getHost() != null) { + sb.append("//"); + String host = uri.getHost(); + // IPv6 handling + if (host.indexOf(':') >= 0 && !host.startsWith("[") && !host.endsWith("]")) { + sb.append('[').append(host).append(']'); } - if (uri.getHost() != null) { - sb.append("//"); - String host = uri.getHost(); - if (host.indexOf(':') >= 0 && !host.startsWith("[") && !host.endsWith("]")) { - sb.append('[').append(host).append(']'); - } - else { - sb.append(host); - } - if (uri.getPort() != -1) { - sb.append(':').append(uri.getPort()); - } + else { + sb.append(host); } - if (uri.getPath() != null) { - sb.append(uri.getPath()); + + if (uri.getPort() != -1) { + sb.append(':').append(uri.getPort()); } - return httpMethod.name() + " " + sb; } - return httpMethod.name() + " " + uri; + if (uri.getPath() != null) { + sb.append(uri.getPath()); + } + return httpMethod.name() + " " + sb; } } From 488ad3d039b2810404d3391e05c42faf8367b8b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EB=8F=99=EC=9C=A4=20=28Park=20Dong-Yun=29?= Date: Sat, 11 Apr 2026 10:36:07 +0900 Subject: [PATCH 3/4] Refactor to remove duplilcated string building logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply feedback for the pr-36641 to reduce wasteful string builing logic Signed-off-by: 박동윤 (Park Dong-Yun) --- .../web/reactive/function/client/WebClientUtils.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java index 05dff9d347ae..50d8a1898fad 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java @@ -72,10 +72,13 @@ public static Mono>> mapToEntityList(ClientResponse r * @since 6.0.16 */ public static String getRequestDescription(HttpMethod httpMethod, URI uri) { + StringBuilder sb = new StringBuilder() + .append(httpMethod.name()).append(" "); + if (uri.getRawQuery() == null) { - return httpMethod.name() + " " + uri; + return sb.append(uri).toString(); } - StringBuilder sb = new StringBuilder(); + if (uri.getScheme() != null) { sb.append(uri.getScheme()).append(':'); } @@ -89,7 +92,7 @@ public static String getRequestDescription(HttpMethod httpMethod, URI uri) { else { sb.append(host); } - + if (uri.getPort() != -1) { sb.append(':').append(uri.getPort()); } @@ -97,7 +100,7 @@ public static String getRequestDescription(HttpMethod httpMethod, URI uri) { if (uri.getPath() != null) { sb.append(uri.getPath()); } - return httpMethod.name() + " " + sb; + return sb.toString(); } } From b04bf0c52a682d38e46ad5c1b598f8cfc2dd0760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EB=8F=99=EC=9C=A4=20=28Park=20Dong-Yun=29?= Date: Sun, 12 Apr 2026 10:44:35 +0900 Subject: [PATCH 4/4] Sanitize URI in getRequestDescription logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prior to this commit, getRequestDescription preserved userInfo, query, and fragment components in the logged URI string. This risked leaking credentials or sensitive query parameters into application logs. This commit strips userInfo, query, and fragment from the log description even if URIs contain only userInfo or fragment (without a query). Signed-off-by: 박동윤 (Park Dong-Yun) --- .../reactive/function/client/WebClientUtils.java | 9 ++++++--- .../function/client/WebClientUtilsTests.java | 14 +++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java index 50d8a1898fad..886ad173bc01 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java @@ -69,13 +69,16 @@ public static Mono>> mapToEntityList(ClientResponse r /** * Return a String representation of the request details for logging purposes * in "METHOD URI" format. + * For the Security purpose, URI is returned in encoded format, + * while userInfo, query, and fragment is stripped out. * @since 6.0.16 */ public static String getRequestDescription(HttpMethod httpMethod, URI uri) { StringBuilder sb = new StringBuilder() .append(httpMethod.name()).append(" "); - if (uri.getRawQuery() == null) { + // also handles Opaque URI, which has only schemeSpecificPart + if (uri.getRawUserInfo() == null && uri.getRawQuery() == null && uri.getRawFragment() == null) { return sb.append(uri).toString(); } @@ -97,8 +100,8 @@ public static String getRequestDescription(HttpMethod httpMethod, URI uri) { sb.append(':').append(uri.getPort()); } } - if (uri.getPath() != null) { - sb.append(uri.getPath()); + if (uri.getRawPath() != null) { + sb.append(uri.getRawPath()); } return sb.toString(); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientUtilsTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientUtilsTests.java index eefa0a69bce0..5d4819355745 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientUtilsTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientUtilsTests.java @@ -53,10 +53,10 @@ void preservesPort() { } @Test - void decodesPathWhenStrippingQuery() { + void remainsPathEncodedWhenStrippingQuery() { URI uri = URI.create("https://host/hello%20world?q=1"); assertThat(WebClientUtils.getRequestDescription(HttpMethod.GET, uri)) - .isEqualTo("GET https://host/hello world"); + .isEqualTo("GET https://host/hello%20world"); } @Test @@ -67,10 +67,10 @@ void stripsUserInfoWithQuery() { } @Test - void preservesUserInfoWithoutQuery() { + void stripsUserInfoWithoutQuery() { URI uri = URI.create("https://admin:secret@host/api"); assertThat(WebClientUtils.getRequestDescription(HttpMethod.GET, uri)) - .isEqualTo("GET https://admin:secret@host/api"); + .isEqualTo("GET https://host/api"); } @Test @@ -81,17 +81,17 @@ void stripsFragmentWithQuery() { } @Test - void preservesFragmentWithoutQuery() { + void stripsFragmentWithoutQuery() { URI uri = URI.create("https://host/page#section"); assertThat(WebClientUtils.getRequestDescription(HttpMethod.GET, uri)) - .isEqualTo("GET https://host/page#section"); + .isEqualTo("GET https://host/page"); } @Test void questionMarkInFragmentNotTreatedAsQuery() { URI uri = URI.create("https://host/page#frag?param=value"); assertThat(WebClientUtils.getRequestDescription(HttpMethod.GET, uri)) - .isEqualTo("GET https://host/page#frag?param=value"); + .isEqualTo("GET https://host/page"); } @Test