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) {
},
error: function (request, status, error) {
}
});
});
... 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 can be found here.
The Solution
However, I’ve created a nuget package that will help you solve it.
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}");
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 has 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/';
}
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 has 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 on github along with a sample project. The library is released using the Apache license.