From 865cf3c117faed584f819e8706d301819b4cc7a1 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] 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. This commit also strips userInfo, query, and fragment from the log description even if URIs contain only userInfo or fragment (without a query). Closes gh-36641 Signed-off-by: 박동윤 (Park Dong-Yun) --- .../function/client/WebClientUtils.java | 39 ++++-- .../function/client/WebClientUtilsTests.java | 127 ++++++++++++++++++ 2 files changed, 158 insertions(+), 8 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..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 @@ -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,18 +67,43 @@ 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. + * 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) { - if (StringUtils.hasText(uri.getQuery())) { - try { - uri = new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), null, null); + StringBuilder sb = new StringBuilder() + .append(httpMethod.name()).append(" "); + + // also handles Opaque URI, which has only schemeSpecificPart + if (uri.getRawUserInfo() == null && uri.getRawQuery() == null && uri.getRawFragment() == null) { + return sb.append(uri).toString(); + } + + 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(']'); } - catch (URISyntaxException ignored) { + else { + sb.append(host); } + + if (uri.getPort() != -1) { + sb.append(':').append(uri.getPort()); + } + } + if (uri.getRawPath() != null) { + sb.append(uri.getRawPath()); } - return httpMethod.name() + " " + uri; + 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 new file mode 100644 index 000000000000..5d4819355745 --- /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 remainsPathEncodedWhenStrippingQuery() { + URI uri = URI.create("https://host/hello%20world?q=1"); + assertThat(WebClientUtils.getRequestDescription(HttpMethod.GET, uri)) + .isEqualTo("GET https://host/hello%20world"); + } + + @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 stripsUserInfoWithoutQuery() { + URI uri = URI.create("https://admin:secret@host/api"); + assertThat(WebClientUtils.getRequestDescription(HttpMethod.GET, uri)) + .isEqualTo("GET https://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 stripsFragmentWithoutQuery() { + URI uri = URI.create("https://host/page#section"); + assertThat(WebClientUtils.getRequestDescription(HttpMethod.GET, uri)) + .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"); + } + + @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 "); + } + +}