Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support stricter encoding of URI variables in UriComponents [SPR-17039] #21577

Closed
spring-issuemaster opened this issue Jul 13, 2018 · 10 comments

Comments

Projects
None yet
2 participants
@spring-issuemaster
Copy link
Collaborator

commented Jul 13, 2018

Rossen Stoyanchev opened SPR-17039 and commented

Historically UriComponents has always encoded only characters that are illegal in a given part of the URI (e.g. "/" is illegal in a path segment), and that does not include characters that are legal but have some other reserved meaning (e.g. ";" in a path segment, or also "+" in a query param).

UriComponents has also always relied on expanding URI variables first, and then encoding the expanded String, which makes it impossible to apply stricter encoding to URI variable values which is usually what's expected intuitively, because once expanded it's impossible to tell the values apart from the rest of the template. Typically the expectation is that expanded values will have by fully encoded.

While the RestTemplate and WebClient can be configured with a UriBuilderFactory that supports different encoding mode strategy, currently there is really no answer when using UriComponents directly.


Affects: 5.0.7

Issue Links:

  • #21565 HtmlUnitRequestBuilder decodes plus sign in query parameter ("is depended on by")
  • #22161 UriComponentsBuilder.toUriString() is broken
  • #21399 Spring is inconsistent in the encoding/decoding of URLs ("supersedes")
  • #20750 Encoding of URI Variables on RestTemplate ("supersedes")
  • #21259 UriComponentsBuilder does not encode "+" properly ("supersedes")

1 votes, 9 watchers

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

commented Jul 17, 2018

Rossen Stoyanchev commented

This is now ready, see the updated "URI Encoding" in the docs. The short version is, use the new UriComponentsBuilder#encode method and not the existing one in UriComponents, i.e. invoke encode before and not after expanding URI variables.

Please give this a try with 5.0.8 or 5.1 snapshots to confirm how it works in your application.

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

commented Jul 17, 2018

Christophe Levesque commented

Thanks Rossen Stoyanchev! The only downside is that it requires that extra UriComponentsBuilder#encode call.

UriComponentBuilder.fromHttpUrl(url).queryParam("foo", foo).toUriString(); // <= this would still not work, needs to add new encode() after toUriString

Is there a way to change the toUriString method in a way that would have the previous code work as is?

PS: Also, unrelated request: can there be a toUri() shorthand method in UriComponentsBuilder the same way there is a toUriString()? :)

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

commented Jul 17, 2018

Rossen Stoyanchev commented

The key to understand this, is that different degrees of encoding are applied to the URI template vs URI variables. In other words given:

http://example.com/a/{b}/c?q={q}&p={p}

The URI template is everything except for the URI variable placeholders. However the code snippet you showed only builds a URI literal without any variables, so the level encoding is the same, only illegal characters, no matter which method is used.

So it would also have to be something like:

.queryParam("foo", "{foo}").buildAndExpand(foo)

By the time .toUriString() is called, the expand would have happened and at that point it's too late to encode URI variables more strictly. Unfortunately we cannot switch the default mode of encoding in UriComponentsBuilder at this stage since that's the only way toUriString() could work without a call to encode().

That said UriComponentsBuilder does have a buildAndExpand shortcut to URI:

URI uri = UriComponentBuilder.fromHttpUrl(url).queryParam("foo", "{foo}").encode().buildAndExpand("a+b");
@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

commented Jul 18, 2018

Rossen Stoyanchev commented

Come to think of it, this works and it's almost identical length as what you had:

URI uri = UriComponentsBuilder
        .fromHttpUrl(url).queryParam("foo", "{foo}").build(foo);

Or include it in the URI template:

URI uri = UriComponentsBuilder
        .fromHttpUrl(url + "?foo={foo}").build(foo);

Explanation: the build(Object...) and build(Map<String, Object>) methods had to be implemented as part of UriBuilder (new in 5.0) but that contract is more likely to be used through DefaultUriBuilderFactory, if at all, and is overall quite new. So I've switched those methods internally to do encode().buildAndExpand().toUri().

For toUriString() a switch from build() + encode() to encode() + build() would make no difference, because no URI variables are expanded. We could add a toUri() as well but that would also have no effect on encoding, i.e. same as toUriString().

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

commented Jul 19, 2018

Rossen Stoyanchev commented

Resolving but now is a good time to try this.

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

commented Nov 8, 2018

Michal Domagala commented

I use autoconfigured WebTestClient in my integration test. I was satisfied with EncodingMode.URI_COMPONENT because I could easy test trimming spaces in request argument - I can just add a space to my request .queryParam("foo", " bar")) and verify space is trimmed on server side.

Could you point me how to elegant undo #21577 for autoconfigured WebTestClient? The only way I found is drop autoconfigured one and create custom client.

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

commented Nov 9, 2018

Rossen Stoyanchev commented

Michal Domagala, I've create a ticket in Boot. I've also included at least one example there of how it can be done. There may be better ways though so watch for updates on the ticket.

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

commented Nov 20, 2018

Leonard Brünings commented

Rossen Stoyanchev please advice on how to solve it with generic UriTemplate

    // templates are populated via Spring Boot Configuration Properties
    private Map<String, UriTemplate> templates = new HashMap<>();

    public URI getLink(String linkType, Map<String, String> templateParams) {
        return templates.get(linkType).expand(templateParams);
    }
conf:
  links:
    configSite: "https://example.com/config?email={email}"

At this point we don't know what variable we have, and if they are query or path variables.

URI redirectUri = getLink("configSite", Collections.singletonMap("email", "service+bar@gmail.com"));
// render     https://example.com/config?email=service+bar@gmail.com
// instead of https://example.com/config?email=service%2Bbar%40gmail.com

This will provide a valid URI, however it won't encode the + and @, and then the plus will get decoded to a space on the receiving site. The problem is that we can't even use URLEncoder.encode manually on the calling site, as this will cause double encoding.

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

commented Nov 21, 2018

Leonard Brünings commented

I managed to get the desired result with:

public URI getLink(String linkType, Map<String, String> templateParams) {
    return UriComponentsBuilder.fromUriString(templates.get(linkType).toString())
            .encode()
            .buildAndExpand(templateParams).toUri();
}

However, I must say this is quite ugly. Is there any way we could get UriTemplate.encode().expand()?

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

commented Nov 21, 2018

Rossen Stoyanchev commented

Not really, UriTemplate uses UriComponentsBuilder internally, and provides just one way of doing it. You could shorten your example to:

public URI getLink(String linkType, Map<String, String> templateParams) {
        return UriComponentsBuilder.fromUriString(templates.get(linkType).toString())
                .build(templateParams);
    }

@spring-issuemaster spring-issuemaster added this to the 5.1 RC1 milestone Jan 11, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.