Last night I spent all evening (and well into the night) working on a fix for a customer. This customer was using the jQuery File Uploader control to upload pictures to their remote server that’s associated with a customer’s order. The upload control was not reflecting the upload status in the built-in progress-bar. It was immediately advancing to 100% and holding until the upload actually completed. After which point the user would be redirected to the appropriate destination page. This left the potential for users to become confused as to whether the browser was “locked up” and start clicking around and disrupting the upload process. The request was simple: fix the control to reflect the actual upload progress. What I encountered while trying to fix this solution was anything but simple.
All modern browsers implement the Same Origin Policy. This policy was introduced as a means to help secure browsers by preventing content sharing by unrelated sites in order to maintain confidentiality and prevent loss of data. In this context, this means that my standard
XmlHttpRequests that are used to invoke the AJAX calls to my remote web service will be blocked by the browser because the destination origin does not match the source web site origin.
There have been two attempts to circumvent this problem: Cross-Origin Resource Sharing (CORS) and JSONP. There are legitimate use cases (such as the one I’m dealing with) where submitting requests to a remote domain without proxying is preferred. Think about how web mashups work - they involve a huge amount of cross-site requests, so being able to invoke remote services without frames is essential.
The problem with JSONP is that it only supports the
GET method and does not use
XmlHttpRequest. However, CORS is not supported in legacy browsers, whereas JSONP does work with legacy browsers. Ultimately, I feel the level of complexity of this upload control necessitates a modern browser that supports CORS. Using
In order to support CORS, your web service has to send back certain HTTP headers that indicate to the browser what methods, origins, and other security policies are allowed by the web service. Without going into too much detail, i suggest you read a much better article on CORS on the Mozilla site.
Thankfully, the jQuery File Uploader control has does support cross-domain requests in more ways than one. In fact, the demo they have posted on the site is issuing a cross-domain request from blueimp.github.com to jquery-file-upload.appspot.com. The default method is via an
XmlHttpRequest (XHR) to a CORS-enabled web service. The alternate method is to use an iFrame transport method. According to their documentation, this is used for browsers like Internet Explorer and Opera which do not yet support XHR file uploads. However, you can configure the file upload control to force iFrame transport by using the
This is what the previous developer had done. He was forcing the iFrame transport because he had not enabled the web service to send the CORS-required headers to allow uploads via XHR.
Core of the Problem
done action fired.
I opted to drop the
forceIframeTransport option and try to get things working using XHR.
Sending the Headers via an ASHX Service
As mentioned previously, the original developer spun up an ASHX service to handle this request. I feel that since this is a .Net 4.0 project we could have created an ASP.NET Web API project since we’re dealing with JSON requests, but I’ll assume that the developer is unfamiliar with the ease of those types of projects and reverted to what he knew. Fair enough argument.
The service handler is fairly simple. Thankfully, the developer separated the front-end logic (of the service) from the business processing logic, albeit into a static method. grumbles
In order to make this web service CORS-enabled, we need to send back the
Access-Control-Allow-Headers and the
Access-Control-Allow-Origin headers. So, a simple refactoring led me to the following that I invoke from the
I started the debugger in Cassini and everything started working beautifully! It was time to deploy to IIS.
IIS and OPTIONS Requests
While using Cassini to test the changes to the ASHX services, I had no problems getting a successful response to the
OPTIONS request submitted by the jQuery File Uploader. However, when I published to IIS, I ran into another wall - my
OPTIONS request to the IIS-hosted service was not returning the custom headers
Access-Control-Allow-Origin, despite getting a 200-level response. In fact, the request and response just showed the following headers:
Why weren’t my custom headers coming across the wire? When I attached the debugger, I found that my ASHX web service was not even being invoked when I tried to use the control, despite seeing the
OPTIONS request coming through my network console. I knew then that IIS was preventing or intercepting the request, so I decided to investigate the handlers for IIS. For reference, the web service is hosted on a web site with an Application Pool running .Net Framework 4 - Integrated Managed Pipeline.
When looking at the lionheart.local Home screen in IIS, double-click the Handler Mappings.
From there, look through the list until you find a handler named
SimpleHandlerFactory-Integrated-4.0 with a path of
\*.ashx and double-click that handler.
In the Edit Managed Handler window that pops up, click the Request Restrictions button.
In the window that pops up (Request Restrictions), click the Verbs tab.
This confirms my suspicions - the handler is not setup to pass
OPTIONS requests to the actual web service. The simple fix is to add
OPTIONS to the list of verbs to respond. However, this is just a temporary fix, as every time I re-deploy the web service, this setting would be reverted. Thus, the more permanent fix is to add the following to the project’s
<system.webServer>) so that this permanently sticks:
Upon re-deploying the service, the
OPTIONS request finally returned the proper headers:
ASHX Response.StatusCode and Response.End()
After pushing these last changes to IIS, I was able to watch the progress bar advance at the correct pace while the file upload progressed. Yet, I ran into yet another issue. Once the upload completed, the network response did not respond with an HTTP Success status code. In fact, it looked like from the network console in Chrome that the web service arbitrarily terminated the connection. No response headers were even registered in the network console.
I attached the debugger to the IIS process and observed that it executed the code without throwing an exception. In fact, the IIS logs show an HTTP status code value of 200. I was completely at a loss and had to think about this for about 15 minutes before checking out what
HttpContext.Current.Response.End() was doing. I came across a Microsoft KnoweldgeBase Article indicating that
Response.End() will throw a
ThreadAbortException. I began to think about why we even needed this invocation. All raw web services I’ve dealt with since .Net 2.0 have never required forcing the response to end. Simply returning from the service will wrap up all necessary ends and send the data back to the client. That’s when I looked at the MSDN Documentation and found this:
This method is provided only for compatibility with ASP.Net, that is, for compatibility with COM-based Web-programming technology that preceded ASP.NET. If you want to jump ahead to the EndRequest event and send a response to the client, call CompleteRequest instead. To mimic the behavior of the End method in ASP, this method tries to raise a [ThreadAbortException] exception. If this attempt is successful, the calling thread will be aborted, which is detrimental to your site’s performance. In that case, no code after the call to the End method is executed.
This isn’t classic ASP, so dumping this method call was the first thing for me to do. Yet, that didn’t fix the problem. I still did not receive any response headers in my network console. So, in digging around a little more, I decided to try to force send a
ResponseCode of 200 back to the client after the upload completed. I literally jumped out of my seat when the upload succeeded, the network console registered a 200-level response, and kicked off the
done event to send me to the destination confirmation page.
The final code for the web service that made all of this work is below.