Monday, April 28, 2014

Basic HTTP Client

Last year, the Android team recommended the use of java.net.HttpURLConnection instead of Apache HttpClient for Gingerbread and up. Unfortunately, HttpURLConnection is a lower-level API, so now everyone has to do their own URL encoding, set MIME type application/x-www-form-urlencoded, read InputStream and ErrorStream, wrap with try/catch, finally close streams, etc.
Rather than write all that code one-off for each request, I’ve created a basic HTTP client which lets you easily
  • make GET, POST, PUT, and DELETE requests
  • make asynchronous requests with automatic retries and exponential backoff
  • customize requests using a RequestHandler
  • automatically wrap requests in an AsyncTask (Android-specific)
It has a very simple API, and the jar weighs in at only 40kB (basic) or 45kb (Android), including source. It is Android-independent by design, although Android developers will find it especially useful.

Basic usage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Example code to login to App Engine dev server
public void loginDev(String userEmail) {
    BasicHttpClient httpClient = new BasicHttpClient("http://localhost:8888");
    ParameterMap params = httpClient.newParams()
            .add("continue", "/")
            .add("email", userEmail)
            .add("action", "Log In");
    httpClient.addHeader("someheader", "value");
    httpClient.setConnectionTimeout(2000); // 2s
    HttpResponse httpResponse = httpClient.post("/_ah/login", params);
    System.out.println(httpResponse.getBodyAsString());
}
 
// Example code to log in to App Engine production app with an auth token
// from Android's AccountManager
public void loginProd(String authToken) {
    BasicHttpClient httpClient = new BasicHttpClient("http://localhost:8888");
    ParameterMap params = httpClient.newParams()
            .add("auth", authToken);
    HttpResponse httpResponse = httpClient.get("/_ah/login", params);
}

Make an asynchronous request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
AndroidHttpClient httpClient = new AndroidHttpClient("http://192.168.1.1:8888");
httpClient.setMaxRetries(5);
ParameterMap params = httpClient.newParams()
        .add("continue", "/")
        .add("email", "test@example.com")
        .add("action", "Log In");
httpClient.post("/_ah/login", params, new AsyncCallback() {
    @Override
    public void onComplete(HttpResponse httpResponse) {
        System.out.println(httpResponse.getBodyAsString());
    }
    @Override
    public void onError(Exception e) {
        e.printStackTrace();
    }
});
To make an asynchronous request, you invoke the request methods (get, post, etc.) with an additional argument, an AsyncCallback, which can be easily implemented in an anonymous inner class as shown above. Here’s a quick tour of the various clients you can use.

BasicHttpClient

BasicHttpClient has no Android dependencies so it can be used on any platform which provides java.net.HttpURLConnection. It offers a synchronous interface only via get, post, put, and delete methods. Note that there are two post methods, one for posting form data (a ParameterMap) and one for uploading content of any type you specify. If you prefer, you can make a new HttpGetRequest and execute() it instead of using the get() method. In fact, all the request methods do this under the covers.

AndroidHttpClient

AndroidHttpClient extends both AbstractHttpClient and AsyncHttpClient so it can used synchronously or asynchronously. Remember that you should not invoke the immediate (non-async) methods on the UI thread. Android will throw an exception if you do. Besides automatically wrapping requests in an AsyncTask via the async methods, AndroidHttpClient automatically includes the workaround for a major bug in HttpURLConnection in earlier versions of Android as discussed in the Android Developers blog post above.

Cookie management

AbstractHttpClient (the base class for everything) registers a default java.net.CookieManager that is used by all HttpURLConnection requests in your VM (= app on Android). The default CookieManager acts just like a browser, remembering cookies it receives and sending them on subsequent requests. You shouldn’t need to set or read cookie headers manually at all. In fact, you can’t. When the CookieManager is in use, it consumes the cookie headers.

Under the covers

AbstractHttpClient is the base client from which all the others flow. It implements a synchronous API and can be instantiated using an anonymous inner class if you just want the base functionality: new AbstractHttpClient() {}.
AsyncHttpClient adds an asynchronous API where each request method takes an extra callback argument. It is intended to be extended like AndroidHttpClient. Subclasses should provide a platform-specific wrapper for async requests. AndroidHttpClient provides an AsyncTask wrapper that AsyncHttpClient uses to make requests in the background.
BasicHttpClient and AndroidHttpClient both delegate the actual I/O to a BasicRequestHandler, which implements a simple request lifecycle (open, prepare, write, read, error). You can construct any of the clients with a custom RequestHandler. BasicRequestHandler is abstract so you can easily override only one method using an anonymous inner class. This is shown on the project home page. A common use for this may be to provide your own onError() method which determines whether a given error condition is recoverable for your application.

Testing

There are no automated tests yet because I haven’t made the effort to simulate connections, timeouts, etc. I have done considerable manual testing, however, of the async capabilities with retries. The basic recipe is as follows:
  • Try an async request to an unreachable IP address (say, 10.0.0.1 or 192.168.1.1). This lets you observe connection timeouts and retries.
  • Copy the SleepServlet in the test package to a Web project and add the servlet mapping in web.xml. Start the server and try an async request. This lets you observe read timeouts and retries.
  • Try an async request to a functioning URL and observe success.

Summary

The code is copiously commented with hopefully non-trivial observations about function and intended use. Planned enhancements include a MappingJsonClient which can take POJO arguments instead of raw bytes or Strings. Spring for Android already has a nice RestTemplate, but like Apache HttpClient, is fairly heavyweight on account of its configurability.
Enjoy!

No comments:

Post a Comment