OkHttp’s New URL Class

Java URLs are painful. HttpURL is here to help.

Written by Jesse Wilson.

Android and Java developers have several options when it comes to manipulating URLs:

  • java.net.URL is 20 years old and shows its age. It works but suffers some implementation problems.
  • java.net.URI is too strict, because it rejects real-world URLs like this one, and too lenient, because it accepts partial URLs like “”.
  • android.net.Uri is optimistic: It doesn’t validate its input. This saves unnecessary work in some situations but limits its utility in others.

Within OkHttp, we’ve always used java.net.URL as our preferred model. It works, but it’s a hassle; each method is almost right but not quite. So, we lean on helper methods and workarounds to get the behavior we want. For example, this is our code to get a URL’s port:

public static int getEffectivePort(URL url) {
int specifiedPort = url.getPort();
return specifiedPort != -1
? specifiedPort
: getDefaultPort(url.getProtocol());
}
public static int getDefaultPort(String protocol) {
if ("http".equals(protocol)) return 80;
if ("https".equals(protocol)) return 443;
return -1;
}

We are tired of the workarounds. So in OkHttp 2.4, we created our own URL model, HttpUrl. It improves upon its predecessors in four important ways.

1. Parsing URLs

What’s the worst thing about being a Java programmer? Catching exceptions that cannot possibly be thrown:

public URL makeYouCry() {
try {
return new URL("https://youtube.com/watch?v=dQw4w9WgXcQ");
} catch (MalformedURLException e) {
throw new AssertionError("say goodbye");
}
}

Our new HttpUrl class doesn’t force you to deal with a MalformedURLExceptionor URISyntaxException. Instead, parse() just returns null when it doesn’t understand what you passed it. The lack of an exception means we can finally declare URL constants:

public static final HttpUrl ANDROID_DOWNLOAD_URL = HttpUrl.parse(
"https://play.google.com/store/apps/details?id=com.squareup.cash");

The parser is strict enough to produce only well-formed URLs, but lenient enough for raw user input. It’s suitable for use in a browser’s address bar and consistent with URL parsers in major web browsers.

2. Canonicalizing URLs

Let’s parse some URLs, add them to a set, and print the result. This is an easy way to see how equals() is implemented.

Set<URL> set = new LinkedHashSet<>();
set.add(new URL("http://Square.GitHub.io/"));
set.add(new URL("http://square.github.io:80/"));
set.add(new URL("http://google.github.io/"));
System.out.println(set);

The first two URLs in the example are semantically equal: They have the same hostname (hostnames are case-insensitive), and the same port (since 80 is the default for HTTP). The third URL is different.

But java.net.URL treats all three URLs as equal, because square.github.io and google.github.io are hosted at the same IP address! The above program prints this:

[http://Square.GitHub.io/]

Ew! Calling URL.equals() also does a DNS lookup, which is bad for both performance and correctness. This is a longstanding bug, and it’s bizarre that it remains unfixed two decades later.

Neither URI and Uri canonicalize their input. So although the first two URLs are semantically equivalent, they aren‘t equal. For those two models, the program prints:

[http://Square.GitHub.io/, http://square.github.io:80/, http://google.github.io/]

With HttpUrl, we do light canonicalization of input URLs. It prints two values:

[http://square.github.io/, http://google.github.io/]

A good equals() method means that HttpUrl is suitable for use as a key in a LinkedHashMap or even a Guava Cache.

3. Query Parameters

Java’s built-in URL classes lack any ability to extract the query parameters from a URL. Suppose you have a URL like this one for a Twitter search:

https://twitter.com/search?q=cute%20%23puppies&f=images

Calling getQuery() or getRawQuery() returns a single string with all parameters glued together:

q=cute%20%23puppies&f=images

This is awkward. Fortunately HttpUrl can decompose the query without fuss:

HttpUrl url = HttpUrl.parse(
"https://twitter.com/search?q=cute%20%23puppies&f=images");
System.out.println(url.queryParameter("q"));
System.out.println(url.queryParameter("f"));

The queryParameter() method extracts the requested value and decodes it. The above code prints this:

cute #puppies
images

There’s also a method to retrieve the query parameter without decoding it (if you’re into that sort of thing).

4. HttpUrl.Builder

Just as the HttpUrl class lets you decompose a URL into its scheme, host, path, query, and fragment, its builder can compose a URL from raw materials.

HttpUrl url = new HttpUrl.Builder()
.scheme("https")
.host("www.google.com")
.addPathSegment("search")
.addQueryParameter("q", "polar bears")
.build();

The builder accepts each component in either its semantic or encoded form. It won’t double encode the percent character, or misinterpret a plus as a space. The builder also makes it easy to build upon an existing URL:

public static final HttpUrl APP_DETAILS_URL =
HttpUrl.parse("https://play.google.com/store/apps/details");
public HttpUrl playStoreUrl(String appId) {
return APP_DETAILS_URL.newBuilder()
.setQueryParameter("id", appId)
.build();
}

Get it in OkHttp

Get OkHttp 2.4.0 on GitHub. It has just what you’ll need to make HTTP requests in Java and Android applications.


Show your support

Clapping shows how much you appreciated Square Engineering’s story.