jamshid
jamshid

Reputation: 1775

Any way to send %2b (encoded plus sign) in query arg with java.net.URI?

I can't seem to find this mentioned explicitly, but it seems that you can't send an escaped plus sign ("%2b") as a query arg value if you're using java.net.URI, since the query arg is escaped.

// bad: http://example.com/foo?a=%252b
new URI("http", null, "example.com", 80, "/foo", "a=%2b", null);

Tried an actual "+" character, but that gets sent as-is, so the server will interpret it as a space.

// bad: http://example.com/foo?a=+
new URI("http", null, "example.com", 80, "/foo", "a=+", null);

So I guess you just have to do the percent-encoding of the query arg keys and values yourself and use the single-argument URI constructor that doesn't escape? Maybe let URI escape the "path", since the rules are tricky (eg, the "+" character means a plus character, not a space, when in the path):

// good: http://example.com/foo?a=%2b
new URI(new URI("http", null, "example.com", 80, "/foo", null, null).toASCIIString() + "?a=%2b");

Also, the docs claim you can create a URI like this and it will be identical to the source URI:

URI u = ...;
URI identical = new URI(u.getScheme(),
        u.getUserInfo(),
        u.getPath(), u.getQuery(),
        u.getFragment());

but that's not true when it contains %2b

URI u = new URI("http://example.com:80/foo?a=%2b");
URI identical = ...; // not identical! http://example.com:80/foo?a=+

Frustrating, I guess that's why everyone uses apache commons or spring classes instead?

PS: http://docs.oracle.com/javase/6/docs/api/java/net/URI.html references a URI constructor that does not exist in "the following identities also hold" section. It needs to remove the "authority" parameter.

Upvotes: 12

Views: 10363

Answers (4)

Stéphane Millien
Stéphane Millien

Reputation: 3448

If you want + character encoded as %2B, here is a solution without third-party library:

URI uri = new URI("http", null, "example.com", 80, "/foo", "a=+", null);
uri = URI.create(uri.toASCIIString().replace("+", "%2B"));

Upvotes: 2

Nirro
Nirro

Reputation: 769

I run into this problem using Spring 5's RestTemplate which uses apache's HttpClient underneath. Whatever I did I could not send %2B which was a problem for Couchbase's non-standard (ugh...) REST API.

This API expects whitespaces in document IDs to be escaped as + on the URL and the literal plus to be an %2B.

Unfortunately, ProtocolExec on the HttpClient request execution chain rewrites all URI's in a way that make it impossible to send a literal %2B on the URI's path.

  • Literal + is sent as +.
  • %2B is also sent as +
  • %252B (which is %2Bwith the % escaped) is sent literally as %252B

My solution, not far from being a hack but also difficult to avoid, was to write a custom HttpClientBuilder and insert a decorator for the MainExec, which would be executed right after the ProtocolExec's rewriting. It looks like this:

/*
 * This is a hack to circumvent Apache's HttpClient otherwise inevitable URI rewriting. This rewriting made it
 * impossible to send a literal %2B in a query path as it includes a forceful reencoding that doesn't reencode the
 * '+' character. This is necessary because Couchbase decided that they were too good for following standards and
 * decided to use + as meaning " " (whitespace) in URI paths instead of the typical %20.
 *
 * As an ugly solution we wrote an HttpClientBuilder that injects a ClientExecChain that will rewrite the full path
 * turning + to spaces. Maybe future versions will make this easier to accomplish.
 */
public class CustomHttpClientBuilder extends HttpClientBuilder {

    @Override
    protected ClientExecChain decorateMainExec(final ClientExecChain requestExecutor) {
        return (
                HttpRoute route,
                HttpRequestWrapper request,
                HttpClientContext clientContext,
                HttpExecutionAware execAware) -> {
            UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(request.getURI());
            uriComponentsBuilder.replacePath(request.getURI().getRawPath().replace("+", "%2B"));
            request.setURI(uriComponentsBuilder.encode().build(true).toUri());

            return requestExecutor.execute(route, request, clientContext, execAware);
        };
    }
}

This uses Spring's UriComponentsBuilder but you can replace it with any other URI building class as long as you end up with a properly formatted java.net.URI.

Also, this does it for the path value but it would be the same for the query args. You'd just have to replace those instead of the path.

I'm really hoping no one else loses a week over this ever again.

Upvotes: 2

Esteban Araya
Esteban Araya

Reputation: 29654

I'd use UriBuilder to take care of all characters that need to be encoded.

Upvotes: -2

jkee
jkee

Reputation: 703

I run into same trouble. After some researching, I think that if you need an %2b encoding of plus symbol, it is better not to use URI class as a result. I am using code like this:

URI uri = new URI(...);
String asciiUrl = uri.toASCIIString();
String plusReplaced = asciiUrl.replace("+", "%2b");

Upvotes: 4

Related Questions