Saturday, January 29, 2011

Get JSON data response with JQuery Form plugin for a file upload form


My latest project involves JQuery for UI development. Frankly speaking, I did not have much idea of JQuery before. But I found it very easy to understand. All APIs are well documented with lots of examples available on Internet. The best feature of JQuery is its plugins. There are thousands of plugins available to play with. For every need of my web app I could find a plugin.



In my web app I am using JQuery Form plugin to submit the forms through AJAX. The return from my servlet is JSON object. The form plugin works great for all my forms. Well almost all, there is a HTML form which has a file upload box along with other input fields. This form was not able to get the response in JSON format. Though it worked in Google Chrome, but Firefox was presenting me with a file save dialog box.
I did a lot of googling to find out whats going on. Finally I observed that the problem is with HTTP headers that the form submit request has. Here are the headers captured by Chrome



Request headers captured by Chrome




As you can see the Accept request header does not contain application/json. That is why the response JSON object is not understood by the browser and it pops up a file save dialog.
When I checked the source code of JQuery Form plugin, I found that in case of a multipart form (i.e. form  having file items) the plugin does not use ajax to submit the form. Rather it uses an IFRAME element. So the form submission is a normal GET/POST request. Here is the part of plugin code that does it -

// are there files to upload?
 var fileInputs = $('input:file', this).length > 0;
 var mp = 'multipart/form-data';
 var multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp);

 // options.iframe allows user to force iframe mode
 // 06-NOV-09: now defaulting to iframe mode if file input is detected
   if (options.iframe !== false && (fileInputs || options.iframe || multipart)) {
    // hack to fix Safari hang (thanks to Tim Molendijk for this)
    // see:  http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
    if (options.closeKeepAlive) {
     $.get(options.closeKeepAlive, fileUpload);
  }
    else {
     fileUpload();
  }
   }
   else {
    $.ajax(options);
   }

Basically its a normal HTTP request not ajax call. So the Accept header is set to application/xml, text/html etc. But the response from the servlet has a Content-Type as application/json. So, browser opens a save as dialog.

To solve this issue in a proper fashion we need to modify the plugin code to upload the file through an ajax request. But files can not be transmitted through ajax. Due to this limitation I had to find another solution which is not as clean as the above one but it will do the job.

The solution is to set the response Content-Type as text/plain. To do this I used below code

if ((null != req.getHeader("Accept"))
                  && req.getHeader("Accept").contains("application/json"))
         {
            resp.setContentType("application/json");
         }
         else
         {
            resp.setContentType("text/plain");
         }
         resp.getWriter().write(jsonResponse);

I checked the request header Accept to see if browser can accept a JSON. If yes, then set the response Content -Type to application/json. Otherwise set it to text/plain. This technique will fool the browser as it falls among the requested types. After this change I captured the response headers

Response headers captured by Chrome



We need to make one more change in the client side code.

var options = { 
         dataType: 'json',
         success: processResponse
     }; 
$('#upload_form_id').ajaxForm(options);


This is important because, without the dataType property, the response will be treated as a text by the plugin. So, the client side script that works on the response of the form submit will not work as it will get a string instead of JSON object.

I checked the method in Firefox 3.6, Chrome 8.0, Internet Explorer 8 and Opera. It worked. I could also maintain my servlet's response type as application/json for all other forms (which does not have a file upload). I hope this works for you too.