OneTrueError - Automated exception handling

How to use CORS requests in Internet Explorer 9 and below.

This article explains how you can automatically proxy CORS requests (Cross-origin resource sharing) in jQuery without changing your existing code. The proxy is integrated in ASP.NET and works with all ASP.NET libraries like WebForms, Mvc and WebApi.

In OneTrueError we have separated the UI from the back-end by using a pure REST service in WebApi. This requires that our UI can use Ajax requests across different sub domains. Unfortunately that’s not supported in Internet Explorer 9 and below. Instead of having to recode all our knockout view models I did another alternative.

I inject logic into the jQuery pipeline which checks the browser version before an Ajax request is made. If the browser is IE9 or below I modify the URI to invoke the CorsProxy instead. This is done in the background without you having to do anything special.

So if you do an ajax request like this ..

 $(function() {
	 $.ajax({
		 url: "http://api.yourdomain.com/user/1",
		 type: 'GET',
		 dataType: 'json',
		 crossDomain: true,
		 data: '22',
		 success: function(result) {
			 //some logic in here.
		 },
		 error: function (request, status, error) {
			 //handle errors here
		 }

	 });

});

.. it will work for all browsers except Internet Explorer (9 and below). In IE9 you will get the “No transport” error message. If you do not want to use text/plain content-type and just GET/POST you have to stop using CORS. More details here.

The solution

However, I’ve created a nuget package that will help you solve it.

cors

Install CorsProxy.aspnet in your front-end project and add the following to your RouteConfig:

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

		//important, must be added before the default route.
        routes.EnableCorsProxy();

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );

    }
}

The CorsProxy will copy all headers and the content from the request to our proxy request and finally copy all information from the proxied response to the ASP.NET response. That means that you can add custom headers, use whatever HTTP method or content you like. The proxy will handle that.

Finally you have to add the JavaScript that injects the proxy into the jQuery ajax handling. Open your BundleConfig and add jquery.corsproxy.js to your jQuery bundle:

public class BundleConfig
{
	public static void RegisterBundles(BundleCollection bundles)
	{
		bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
					"~/Scripts/jquery-{version}.js",
					"~/Scripts/jquery-corsproxy-{version}.js"));
		
		// [...]
	}
}

Done! Now jQuery should gracefully downgrade to proxying for CORS requests if IE9 or below is used.

Implementation

jQuery have a method called $.ajaxSetup which was added in version 1.1. In it there is an option called beforeSend which is invoked before every Ajax request is made. Hence it can be used to check internet explorer version and then change the URI if required. However, since version 1.5 there is another method called $.ajaxPrefilter which is intended to replace $.ajaxSetup. I therefore check if $.ajaxPrefilter is defined or not and use the method that exists.

The trick is to change the URI and then add the original uri as a custom HTTP header.

$.ajaxPrefilter(function (options, originalOptions, jqXhr) {
    if (!window.CorsProxyUrl) {
        window.CorsProxyUrl = '/corsproxy/';
    }
    // only proxy those requests
    // that are marked as crossDomain requests.
    if (!options.crossDomain) {
        return;
    }

    if (getIeVersion() && getIeVersion() < 10) {
        var url = options.url;
        options.beforeSend = function (request) {
            request.setRequestHeader("X-CorsProxy-Url", url);
        };
        options.url = window.CorsProxyUrl;
        options.crossDomain = false;
    }
});

It’s not more complicated than that.

Server-side

At the server side I’ve created a custom IHttpHandler. The great thing with IHttpHandler is that it’s defined in System.Web and will therefore work with all dialects of ASP.NET. The down side is that it requires some additional configuration to be activated compared to IHttpModule. But let’s start by looking at the IHttpHandler implementation:

public class CorsProxyHttpHandler : IHttpHandler
{
	public void ProcessRequest(HttpContext context)
	{
		var url = context.Request.Headers["X-CorsProxy-Url"];
		if (url == null)
		{
			context.Response.StatusCode = 501;
			context.Response.StatusDescription =
				"X-CorsProxy-Url was not specified. The corsproxy should only be invoked from the proxy JavaScript.";
			context.Response.End();
			return;
		}

		try
		{
			var request = WebRequest.CreateHttp(url);
			context.Request.CopyHeadersTo(request);
			request.Method = context.Request.HttpMethod;
			request.ContentType = context.Request.ContentType;
			request.UserAgent = context.Request.UserAgent;
			
			if (context.Request.AcceptTypes != null)
			request.Accept = string.Join(";", context.Request.AcceptTypes);

			if (context.Request.UrlReferrer != null)
				request.Referer = context.Request.UrlReferrer.ToString();

			if (!context.Request.HttpMethod.Equals("GET", StringComparison.OrdinalIgnoreCase))
				context.Request.InputStream.CopyTo(request.GetRequestStream());

			var response = (HttpWebResponse)request.GetResponse();
			response.CopyHeadersTo(context.Response);
			context.Response.ContentType = response.ContentType;
			context.Response.StatusCode =(int) response.StatusCode;
			context.Response.StatusDescription = response.StatusDescription;

			var stream = response.GetResponseStream();
			if (stream != null && response.ContentLength > 0)
			{
				stream.CopyTo(context.Response.OutputStream);
				stream.Flush();
			}
		}
		catch (WebException exception)
		{
			context.Response.AddHeader("X-CorsProxy-InternalFailure",  "false");

			var response = exception.Response as HttpWebResponse;
			if (response != null)
			{
				context.Response.StatusCode = (int)response.StatusCode;
				context.Response.StatusDescription = response.StatusDescription;
				response.CopyHeadersTo(context.Response);
				var stream = response.GetResponseStream();
				if (stream != null)
					stream.CopyTo(context.Response.OutputStream);

				return;
			}

			context.Response.StatusCode = 501;
			context.Response.StatusDescription = exception.Status.ToString();
			var msg = Encoding.ASCII.GetBytes(exception.Message);
			context.Response.OutputStream.Write(msg, 0, msg.Length);
			context.Response.Close();

		}
		catch (Exception exception)
		{
			context.Response.StatusCode = 501;
			context.Response.StatusDescription = "Failed to call proxied url.";
			context.Response.AddHeader("X-CorsProxy-InternalFailure", "true");
			var msg = Encoding.ASCII.GetBytes(exception.Message);
			context.Response.OutputStream.Write(msg, 0, msg.Length);
			context.Response.Close();

		}
	}

	public bool IsReusable { get { return true; }}
}

As you can see it will also handle error responses. The X-CorsProxy-InternalFailure header indicates if it’s the CorsProxy itself that have failed or the server that we call.

The routes.EnableCorsProxy(); is an extension method that adds our custom route to the route table. The route specifies that our CorsProxyRouteHandler should be used. Check the github repository for more information.

Code

The code is available in github along with a sample project. The library is released using the Apache license.

This entry was posted in CodeProject, Libraries and tagged , , , . Bookmark the permalink.
  • Igor Ilyin

    It doesn’t solve the http -> https problem, plus there’s no authentication or any other secure.

    • http://blog.gauffin.org/ jgauffin

      the cookies are copied and included in the proxy request. So ASP.NET Forms authentication should still work.

  • Michael

    You are my new programming hero. Thank you!