JSON requests with HttpURLConnection in Android
http://www.evanjbrunner.info/posts/json-requests-with-httpurlconnection-in-android/
Published: July 17, 2012 |
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
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)