evan j brunner

JSON requests with HttpURLConnection in Android

In the midst of doing something quazi-irrational, I stumbled over the need to create and send JSON requests to a web-server in an android context. Software should be oriented towards the inevitable dominance of a better future, and thus I built the mechanics of this new toy to be dependent on HttpURLConnection, as advised in the Android developer blog:

For Gingerbread and better, HttpURLConnection is the best choice. Its simple API and small size makes it great fit for Android. Transparent compression and response caching reduce network use, improve speed and save battery. New applications should use HttpURLConnection; it is where we will be spending our energy going forward.

-Jesse Wilson (of Dalvik team, but now square)

HttpURLConnection has no nifty little JSON request function, and googling around for a bit didn’t turn up anything too exciting or functional.

The solution is simply to incrementally build a tailored request:

//can catch a variety of wonderful things
try {
  //constants
  URL url = new URL("http://myhost.com/ajax");
  String message = new JSONObject().toString();

  HttpURLConnection conn = (HttpURLConnection) url.openConnection();
  conn.setReadTimeout( 10000 /*milliseconds*/ );
  conn.setConnectTimeout( 15000 /* milliseconds */ );
  conn.setRequestMethod("POST");
  conn.setDoInput(true);
  conn.setDoOutput(true);
  conn.setFixedLengthStreamingMode(message.getBytes().length);

  //make some HTTP header nicety
  conn.setRequestProperty("Content-Type", "application/json;charset=utf-8");
  conn.setRequestProperty("X-Requested-With", "XMLHttpRequest");

  //open
  conn.connect();
  
  //setup send
  OutputStream os = new BufferedOutputStream(conn.getOutputStream());
  os.write(message.getBytes());
  //clean up
  os.flush();
  
  //do somehting with response
  is = conn.getInputStream();
  String contentAsString = readIt(is,len);
} finally {  
  //clean up
  os.close();
  is.close();
  conn.disconnect();
}

note: the streams don’t need to be buffered if they are generally expected to be dealt with in one go, but large messages should be buffered to alleviate device memory load.

The aim is to build something similar to the following HTTP request 1:

POST /ajax HTTP/1.1
Accept: */*
Content-Type: application/json;charset=utf-8
X-Requested-With: XMLHttpRequest
User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.0.4; sdk Build/MR1)
Host: myhost.com
Connection: Keep-Alive
Accept-Encoding: gzip
Content-Length: 123

{"hash":{"attribute":"123"},"attribute":"value", …}

The important attributes are:

content-Type: application/json;charset=utf-8
tell the server to expect a JSON Object
X-Requested-With: XMLHttpRequest
pretend this request was created with an AJAX appropriate API
Content-Length: 123
the byte length of the message
{"hash":{"attribute":"123"},"attribute":"value", …}
the body of the request is a properly formatted JSON Object

The content length figure may appear particularly innocuous, but it is a lurking evil. The tendency when setting up HttpURLConnection is to lean towards using setChunkedStreamingMode(int) as you don’t have to worry about figuring out the precise message length before you send it. This does not work because the Transfer-Encoding: chunked mechanism apparently inserts additional characters (non-JSON [formatting / parsing] friendly) into the message body which can cause the server to fail on interpreting the object – resulting in failure, or even worse: inconsistent JSON interpretation. For example1:

POST /ajax HTTP/1.1
Content-Type: application/json;charset=utf-8
X-Requested-With: XMLHttpRequest
User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.0.4; sdk Build/MR1)
Host: myhost.com
Connection: Keep-Alive
Accept-Encoding: gzip
Transfer-Encoding: chunked

70
{"data":{"password":"123","email":"ejb"},"method":"account.sign_in",…
0

as interpreted on a rails test bench, has the parameters:

Started POST "/ajax" for localhost 2012-07-16 23:48:36 -0700
  Processing by AjaxController#post as */*
  Parameters: {"ajax"=>{"action"=>"post", "controller"=>"ajax"}}
Completed 200 OK in 8ms (Views: 7.2ms | ActiveRecord: 0.0ms)

which is entirely incomplete relative to intention. When the request is assembled (as above) with setFixedLengthStreamingMode(…) the result better fits expectations:

Started POST "/ajax" for localhost at 2012-07-17 01:15:45 -0700
  Processing by AjaxController#post as */*
  Parameters: {"data"=>{"password"=>"[FILTERED]", "email"=>"ejb"}, "me…
Completed 200 OK in 6ms (Views: 4.8ms | ActiveRecord: 0.0ms)

1 Requests were captured by pointing an app at nc -l