jQuery File Upload: IE9 and ASP.Net Web API File Uploading
The blueimp jQuery File Upload plugin uses the XMLHttpRequest to pass the file data to a server (only IE10+). If browser doesn’t support Ajax file uploading, the plugin makes a workaround by dynamically creating IFrame and sending the data on behalf of it through the traditional form POST. The JavaScript responsible for the workaround resides in jquery.iframe-transport.js, which accompanies the basic jquery.fileupload.js. The following TypeScript code could be used to initialize the plugin and submit file data (the code is intentionally kept as simple as possible – no progress bars, validations and so on):
//... private fileData: any = {}; //... $(".filePicker").fileupload({ autoUpload: false, // will be submitted once button is clicked method: "PUT", dataType: "json", url: "api/someController/someMethod", // Web Api method to receive and process the file formData: () => { // additional parameters accompanying the file data return [{ name: "bookName", value: $("#bookName").val() }, { name: "bookGenre", value: $("#bookGenre").val() }, { name: "bookAuthor", value: $("#bookAuthor").val() }]; }, add: (e: JQueryEventObject, data: any) => { // event handlers this.fileData = data; //... }, done: (e: JQueryEventObject, data: any) => { //... }, fail: (e: JQueryEventObject, data: any) => { //... }, always: (e: JQueryEventObject, data: any) => { //... } }); //... $("#loadBook").on('click', function () { // the button to initiate the file sending if (this.fileData && this.fileData.hasOwnProperty("process")) { // file data validation: size, extension, whatever else... this.fileData.process().done(() => { // file sending this.fileData.submit(); }); } });
On the server side the following code receives and processes the file data:
using System; using System.Text; using System·Web; using System·Web.Http; using System.Net; using System.Net.Http; using System.Runtime.Serialization.Json; using System.IO; using System.Linq; ... namespace DotNetFollower.Web.Controllers.Api { [RoutePrefix("api/someController")] public class someController : ApiController { public someController() { //... } [Route("someMethod")] [HttpPut] [HttpPost] // this attribute allows processing traditional form POST public ServiceResult<BookOutput> someMethod() { try { //... var request = HttpContext.Current.Request; var files = request.Files; if (files.Count == 0) throw new Exception("Couldn't find a book to load!"); // read accompanying parameters var bookName = request.Form.Get("bookName"); var bookGenre = request.Form.Get("bookGenre"); var bookAuthor = request.Form.Get("bookAuthor"); var file = new HttpPostedFileWrapper(files[0]); // parsing file.InputStream ... // processing the parsed file data ... file.InputStream.Close(); //... return new ServiceResult<BookOutput>() { Data = new BookOutput() { Name = bookName, Genre = bookGenre, Author = bookAuthor } }; } catch(Exception ex) { return new ServiceResult<BookOutput>(ex); } } } } // Where ServiceResult and BookOutput are defined as follows public class ServiceResult<T> { public bool Success { get; set; } public string ErrorMessage { get; set; } public T Data { get; set; } public ServiceResult() { Success = true; } public ServiceResult(string errorMessage) { ErrorMessage = errorMessage; } public ServiceResult(Exception exception) { ErrorMessage = exception.Message; } } public class BookOutput { public string Name { get; set; } public string Genre { get; set; } public string Author { get; set; } }
Unfortunately, the code doesn’t works as expected in Internet Explorer 9 (thankfully, the lower versions are not supposed to be supported by the project, so I don’t care about them). If IE9 sends an Ajax request to the Web Api method, it interprets the JSON response correctly. However, when uploading a file, the jQuery File Upload plugin sends traditional non-Ajax form POST. So, having received the JSON result, IE9 prompts for a JSON file download.
To bypass such IE9 behaviour the server response should contain the content-type header “text/html” rather than the “application/json” returned by the Web Api method by default. To avoid writing some IE9 specific logic on both server and client sides, I’ve introduced the following Web Api method-adapter:
[Route("someMethodAdapted")] [HttpPut] [HttpPost] // this attribute allows processing traditional form POST public HttpResponseMessage someMethodAdapted() { var res = someMethod(); // call the original method const string JsonContentType = "application/json"; const string HtmlContentType = "text/html"; var response = Request.CreateResponse(HttpStatusCode.OK); // return 200 OK var context = HttpContext.Current; response.Content = new StringContent(ToJson(res), // serialize result object into JSON Encoding.UTF8, // check if the JSON content-type is accepted // (it's not accepted in case of form POST coming from IE9) context.Request.AcceptTypes.Contains(JsonContentType, StringComparer.OrdinalIgnoreCase) ? JsonContentType : HtmlContentType); // return suitable content-type return response; } // the ToJson method is defined as follows private static string ToJson<T>(T obj) where T : class { if (obj == null) return string.Empty; DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(T)); using (MemoryStream stream = new MemoryStream()) { serializer.WriteObject(stream, obj); return Encoding.Default.GetString(stream.ToArray()); } }
The someMethodAdapted is supposed to be used instead of someMethod everywhere on the client side. So, repoint the url to the method-adapter
... $(".filePicker").fileupload({ ... // Web Api method-adapter to receive and process the file url: "api/someController/someMethodAdapted", ... }); ...
The someMethodAdapted makes a content-type trick and perfectly serves IE9 and higher. The use of HttpResponseMessage gives a control over the response headers. The HttpContext.Current.Request.AcceptTypes is a list of client-supported content types (aka MIME types). If the “application/json” is not in the list, the “text/html” is the right choice. Below are the AcceptTypes of Ajax and non-Ajax requests made by IE9:
// IE9 Ajax request to a Web Api method (true for higher browser versions too) HttpContext.Current.Request.AcceptTypes {string[3]} string[] [0] "application/json" string [1] "text/javascript" string [2] "*/*; q=0.01" string // IE9 non-Ajax form POST HttpContext.Current.Request.AcceptTypes {string[3]} string[] [0] "text/html" string [1] "application/xhtml+xml" string [2] "*/*" string