Victor Romero opened SPR-16522 and commented

UriComponentsBuilder doesn't escape pathSegments that have an url inside:

The following code:

URI uri = UriComponentsBuilder.fromHttpUrl("http://127.0.0.1:" + randomServerPort + "/")
              .pathSegment("http://url.com/within")
              .build().toUri();
System.out.println(uri.toString());

Will generate:

http://127.0.0.1:43963/http://url.com/within

Project that reproduces: https://github.com/vromero/webclient-issue-uri-in-uri


Affects: 5.0.4

Reference URL: https://github.com/vromero/webclient-issue-uri-in-uri

Comment From: spring-projects-issues

Rossen Stoyanchev commented

You're missing the call to encode():

URI uri = UriComponentsBuilder.fromHttpUrl("http://127.0.0.1:" + randomServerPort + "/")
        .pathSegment("http://url.com/within")
        .build().encode().toUri();
System.out.println(uri.toString());

Comment From: mauromol

@rstoyanchev please note that as of Spring Framework 5.3.4, the Javadoc of org.springframework.web.util.UriBuilder.pathSegment(String...) is at least misleading, because it says:

If slashes are present in a path segment, they are encoded:

builder.pathSegment("ba/z", "{id}").build("a/b")

// Results is "/ba%2Fz/a%2Fb"

But, from what I see, this is wrong: result is /ba/z/a/b, unless I also call encode() before build().

Comment From: rstoyanchev

build() returns UriComponents and I've no idea what you do then to see this result? If it is followed by toUriString() that simply concatenates as explained in the Javadoc.

Comment From: mauromol

Ok, I understood what cheated me.

If you do:

UriComponentsBuilder b = UriComponentsBuilder.fromHttpUrl("http://www.example.com");
System.out.println(b.pathSegment("ba/z", "{id}").build(Map.of("id", "a/b")));

you'll get: http://www.example.com/ba%2Fz/a%2Fb, which is coherent with the Javadoc.

But if you do:

UriComponentsBuilder b = UriComponentsBuilder.fromHttpUrl("http://www.example.com");
System.out.println(b.pathSegment("ba/z", "{id}") .buildAndExpand(Map.of("id", "a/b")).toUri());

you'll get: http://www.example.com/ba/z/a/b, unless you add encode(), either before or after the call to buildAndExpand():

UriComponentsBuilder b = UriComponentsBuilder.fromHttpUrl("http://www.example.com");
System.out.println(b.pathSegment("ba/z", "{id}").buildAndExpand(Map.of("id", "a/b")).encode().toUri());

I see now that build(Object...) and build(Map<String, ?> uriVariables) return a URI and perform encoding, while build(), build(boolean), buildAndExpand(Object...) and buildAndExpand(Map) return a UriComponents instead and do NOT perform encoding. You must dig into the implementation of UriComponents to understand what is really going on there. From a user point of view, these two variants:

b.pathSegment("ba/z", "{id}").build(Map.of("id", "a/b"))
b.pathSegment("ba/z", "{id}").buildAndExpand(Map.of("id", "a/b")).toUri()

look very similar to me and I can't find anywhere in the Javadoc any "WARNING" about their different behaviour. The Javadoc of build(Map<String, ?> uriVariables) says:

Build(*) a URI instance and replaces URI template variables with the values from a map

while the Javadoc of buildAndExpand(Map<String, ?>) says:

Build a UriComponents instance and replaces URI template variables with the values from a map.

So the only difference is the result type, but having UriComponents a toUri() methods sounds like you should get the same final result, but you won't. No mention at all on what the two methods do w.r.t. encoding. Also, org.springframework.web.util.UriComponents.toUri() seems to suggest that, if the UriComponents is not encoded, the encoding step will be done by the multi-argument URI constructor, but given the example above you see that the result is indeed different. So once again, the user may expect the above two statements to be equivalent, but they are not.

(*) as a final minor note, see the incoherent use of the third-person: "build" - "replaces".