Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand Down Expand Up @@ -69,18 +67,43 @@ public static <T> Mono<ResponseEntity<List<T>>> 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();
}

}
Original file line number Diff line number Diff line change
@@ -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");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could explain why the path needs decoding? I think using the raw path would be consistent with URI#toString().

Copy link
Copy Markdown
Author

@MintBee MintBee Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were two reasons.

  1. Since it was for logging, I though decoded path is more readable.
  2. Controller annotations take decoded string, such as @GetMapping("/hello world"). I though it would be more consistent with the overall API.

I should've made a comment about it. I was too focused on only describing the performance parts. Thanks for pointing that @bclozel

Should I rollback and return the encoded string?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes please, let's use the encoded form. It will be consistent with the URI toString and will avoid log injection / log forging vulnerabilities.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved all what you have said @bclozel. Thanks for helping me a lot! Really appreciate it.

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");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the original issue, you added a comment about this: I think we should indeed strip the user info from the logged URI. Can you update the implementation to check for the presence of raw user info?

Copy link
Copy Markdown
Author

@MintBee MintBee Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I strip the fragment too when only the fragment is in presence for the URI? I guess we should, since this is more consistent with existing API, and the purpose of the method is purely for logging.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes please, let's remove the user info, query and fragment if any of them are present. Thanks!

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 ");
}

}