IE8 and CORS

posted in: Uncategorized | 12

Working with the modern web and APIs is great… if you are only supporting the most up to date browsers. If you need to support IE8, it gets tricky. IE8 has many restrictions on sending AJAX requests including not being a fan of Cross-domains requests, and http-https requests.

BUT WHY IE8

Windows XP, that’s why. Windows XP is still a HUGE player in the OS game, and thanks to some decisions at Microsoft, if you are on Windows XP, you can’t upgrade from IE8 to IE9. Because of this, IE8 still manages to keep a 9.3% market share worldwide, 36.8% if your users are from china.

SEE ALSO:
http://www.troyhunt.com/2013/01/the-impending-crisis-that-is-windows-xp.html
http://gs.statcounter.com/

Assumptions

We’re going to assume the following in this article:

  1. url is a variable containing the url to the API we’re consuming
  2. Data is contained in the url variable (ex: x=1&y=2)
  3. We’re sending GET requests
  4. Success is the callback function that will have the response passed into it
  5. You want to use this on either an HTTP site or an HTTPS website

Normal AJAX Request (using JQuery)

So, for a normal AJAX request using jQuery, we’re gunna use something like this.

$.ajax({
	type: "GET",
	url: url,
	dataType: "html",
	async:true,
	success: function (response){
		success(response);
	}
});

Chrome? All Good.
Firefox? All Good.
IE? NOPE. ACCESS DENIED.

Seriously though, access denied. If you take a look at the network tab in the IE developer tools, it doesn’t even try to send the request. It sees a different domain and says NOPE.

window.XDomainRequest

The IE team decided that instead of allowing an AJAX request to go cross domain, they would implement a new object called a window.XDomainRequest instead. This was introduced in IE8, and changed a few times during the betas before the final mess that it became.

What does it look like to send one of these requests?

var xdr = new XDomainRequest();
xdr.open("get", url);
xdr.onprogress = function () { };
xdr.ontimeout = function () { };
xdr.onerror = function () { };
xdr.onload = function() {
  success(xdr.responseText);
}
setTimeout(function () {xdr.send();}, 0);

Now, you may be wondering a couple things here like why I defined the onprogress, ontimeout, and onerror events as empty functions. Well, because IE. And even more importantly, why did I do a setTimeout of 0 before sending the request? There is a weird bug where the request may get blocked and not send and that was the fix. Seriously.

Feature Detection, Not Browser Detection

So, now we are thinking…

If(IE){
	XDomainRequest
} else {
	$.ajax();
}

However, it is better practice to use feature detection VS browser detection. I’m not going to get too much into this. If you really want to know why, google is your friend and many people have written about it including JQuery’s President Dave Methvin.

So, here’s what we’re actually looking at..

if(window.XDomainRequest){
	var xdr = new XDomainRequest();
	xdr.open("get", url);
	xdr.onprogress = function () { };
	xdr.ontimeout = function () { };
	xdr.onerror = function () { };
	xdr.onload = function() {
	  success(xdr.responseText);
	}
	setTimeout(function () {xdr.send();}, 0);
} else {
	$.ajax({
		type: "GET",
		url: url,
		dataType: "html",
		async:true,
		success: function (response){
			success(response);
		}
	});
}

So, we’re ready to send requests to our API using IE8+ and all modern browsers, right? Not… quite.

HTTP-HTTPS

So, by now if you’ve done any development, you should be aware of what mixed content warnings are and why you shouldn’t mix http with https content.

XDomainRequest will not let you make a request from HTTPS to HTTP, which makes sense. It doesn’t make sense to go from secure to not secure. However, it also doesn’t let you go from HTTP to HTTPS, which personally to me doesn’t make sense. We’re protecting the user from sending a request to a source that is MORE secure than the site we are on?

Iframe Gateway

The workaround for this problem is complicated. To make it work you need to be able to set up an HTTPS folder on your web server (easy in some cases, not so much in others). In this example, we’ll be using a variable called GatewayURL that points to https://www.yourdomain.com/secure/gateway.html. Note the HTTPS at the beginning.

So, the basic idea here is we’re sitting on an HTTP page. We want to go to an HTTPS API. To do this, we need a middle man. The idea here is to inject a hidden iframe into the body to the GatewayURL, send the request to it, and wait for the response. The iframe will forward the request to the API and let us know when it gets a response. The trick here is that since the iframe is HTTPS, it is communicating HTTPS to HTTPS for us.

So. for the IE portion of our code, we need to check the protocol of our current document.

var protocol = location.protocol;
if(protocol == "http:"){
	//iframe code
} else {
	var xdr = new XDomainRequest();
	xdr.open("get", url);
	xdr.onprogress = function () { };
	xdr.ontimeout = function () { };
	xdr.onerror = function () { };
	xdr.onload = function() {
	  success(xdr.responseText);
	}
	setTimeout(function () {xdr.send();}, 0);
}

Now for the iframe injection code…

if(RequestHelper.Busy){
	setTimeout(function(){
		RequestHelper.sendRequest(url,success,$);
	},50);
} else {
	RequestHelper.Busy = true;
	$("body").append("<iframe id="ajaxProxy" style="display: none;" src="&quot;+RequestHelper.GatewayURL+&quot;" width="320" height="240"></iframe>"); 
	$("#ajaxProxy").load(function(){ 
		ajaxProxy.postMessage(url,"*"); 
		$(window).bind("message",function(e){ 
			$("#ajaxProxy").remove(); 
			$(window).unbind("message"); 
			RequestHelper.Busy = false; 
			success(e.originalEvent.data); 
		}); 
	}); 
}

You can see here, the iframe is injected, and onload we’re using the postMessage API to send the url to the iframe. We then bind an event to the window to wait for a message to be sent back. When we get the message back, we remove the iframe, stop listening for a response, and call the callback using the response we got back from the iframe.

The code for the iframe is pretty simple…

$(window).bind("message",function(e){
	var xdr = new XDomainRequest();
	xdr.open("get", e.originalEvent.data);
	xdr.onprogress = function () { };
	xdr.ontimeout = function () { };
	xdr.onerror = function () { };
	xdr.onload = function() {
		parent.postMessage(xdr.responseText,"*");
	}
	setTimeout(function () {xdr.send();}, 0);
});

It’s the exact same code as a normal XDomainRequest, but called when the iframe recieves a message from the parent. When it recieves a response, it called postMessage on the parents and sends back the response it got.

Okay. So we’ve got normal CORS requests, CORS for IE, HTTP-HTTPS requests, we’re all good right?

Almost.

Multiple Requests

Using the code we have, we’re going to run into problems if we try to send multiple requests at the same time in IE. Multiple iframes are going to be created and when each one responses, both listeners for the requests will accept the response. So, if I send two requests, I’m going to get FOUR success callback calls. We need to make it so only one request can be sent at a time using the iframe method.

To do this, we’re going to use a Busy variable. To make things more organized, we’re going to organize everything we have so far into an object as well as add the BUSY property.

var RequestHelper = {
	GatewayURL: "https://www.yourdomain.on.ca/secure/gateway.html",
	Busy: false,
	sendRequest: function(url,success,$){
		var protocol = location.protocol;
		if(window.XDomainRequest){
			if(protocol == "http:"){
				$("body").append("<iframe id="ajaxProxy" style="display: none;" src='"+RequestHelper.GatewayURL+"' width="320" height="240"></iframe>"); 
				$("#ajaxProxy").load(function(){ 
					ajaxProxy.postMessage(url,"*"); 
					$(window).bind("message",function(e){ 
						$("#ajaxProxy").remove(); 
						$(window).unbind("message"); 
						success(e.originalEvent.data); 
					}); 
				}); 
			} else { 
				var xdr = new XDomainRequest(); 
				xdr.open("get", url); 
				xdr.onprogress = function () { }; 
				xdr.ontimeout = function () { }; 
				xdr.onerror = function () { }; 
				xdr.onload = function() { 
					success(xdr.responseText); 
				} 
				setTimeout(function () {
					xdr.send();
				}, 0); 
			} 
		} else { 
			$.ajax({ 
				type: "GET", 
				url: url, 
				dataType: "html", 
				async:true, 
				success: function (response){ 
					success(response); 
				} 
			}); 
		} 
	} 
}

By default, the RequestHelper isn’t busy. What we’re going to do is check if it is busy. If it is, wait 50 milliseconds and try again. If not, set the busy property to true, do the iframe request as usual, and then set the property back to false.

if(RequestHelper.Busy){
	setTimeout(function(){
		RequestHelper.sendRequest(url,success,$);
	},50);
} else {
	RequestHelper.Busy = true;
	$("body").append("<iframe id="ajaxProxy" style="display: none;" src="&quot;+RequestHelper.GatewayURL+&quot;" width="320" height="240"></iframe>"); 
	$("#ajaxProxy").load(function(){ 
		ajaxProxy.postMessage(url,"*"); 
		$(window).bind("message",function(e){ 
			$("#ajaxProxy").remove(); 
			$(window).unbind("message"); 
			RequestHelper.Busy = false; 
			success(e.originalEvent.data); 
		}); 
	}); 
}

Final Code

var RequestHelper = {
	GatewayURL: "https://www.yourdomain.on.ca/secure/gateway.html",
	Busy: false,
	sendRequest: function(url,success,$){
		var protocol = location.protocol;
		if(window.XDomainRequest){
			if(protocol == "http:"){
				if(RequestHelper.Busy){
					setTimeout(function(){
						RequestHelper.sendRequest(url,success,$);
					},50);
				} else {
					RequestHelper.Busy = true;
					$("body").append("<iframe id="ajaxProxy" style="display: none;" src="&quot;+RequestHelper.GatewayURL+&quot;" width="320" height="240"></iframe>"); 
					$("#ajaxProxy").load(function(){ 
						ajaxProxy.postMessage(url,"*"); 
						$(window).bind("message",function(e){ 
							$("#ajaxProxy").remove(); 
							$(window).unbind("message"); 
							RequestHelper.Busy = false; 
							success(e.originalEvent.data); 
						}); 
					}); 
				} 
			} else { 
				var xdr = new XDomainRequest(); 
				xdr.open("get", url); 
				xdr.onprogress = function () { }; 
				xdr.ontimeout = function () { }; 
				xdr.onerror = function () { }; 
				xdr.onload = function() { 
					success(xdr.responseText); 
				} 
				setTimeout(function () {
					xdr.send();
				}, 0); 
			} 
		} else { 
			$.ajax({ 
				type: "GET", 
				url: url, 
				dataType: "html", 
				async:true, success: 
				function (response){ 
					success(response); } 
				}); 
		} 
	} 
}

Conclusion

It is probably under very special circumstances that you’ll need all of this code and have all of these edge cases, but I know that personally it took alot of hours of research to figure this all out, and I wanted to put it all together into one resource.

Also, I hate IE8.

EDIT: Created a GitHub repository for the final code. https://github.com/andrewmcgivery/RequestHelper

EDIT 2:ย I have added documentation for XDomainRequest to MDN

My name is Andrew McGivery. I currently work full time as an application developer at Manulife Financial in Canada. My current passion is building and leading highly engaged teams where employee happiness, learning, and growth is a priority.

12 Responses

  1. Very well written article; this came up during a search today to see if CORS worked in IE8.

    I wanted to reply to something you said in it though, because I’ve had the same point made to me before. You said:
    “However, it also doesnโ€™t let you go from HTTP to HTTPS, which personally to me doesnโ€™t make sense.”

    Actually, this makes a lot of sense. If you can go from HTTP to HTTPS, you can potentially put HTTPS data on an HTTP page, and once it’s on an HTTP page it can be leaked much more easily to other HTTP resources. HTTPS protects against man in the middle attacks; HTTP doesn’t.

    Ultimately the security policy has to work both ways. Going HTTPS to HTTP is always the obvious case, but going HTTP to HTTPS to HTTP actually represents the same concern.

  2. Hello ,
    can you help me to solve xdomain request for json .
    currently I have code which work on ie10 but not working on lower IE version (give error: Acess is denied.),
    here GetFlightHotelSource is static method which return list of data,
    If possible then reply me soon

    $.support.cors = true;
    //FlightHotel Flyfrom
    $.ajax({
    type: “POST”,
    crossDomain: true,
    url: “http://test.a1travel.com/WebMethods.aspx/GetFlightHotelSource”,
    datatype: “json”,
    //data: “{‘countryMasterId’:'” + CountryId + “‘}”,
    contentType: “application/json; charset=utf-8”,
    success: function(result) {
    //fill dropdownlist
    FillFlightHotelSource(result.d);
    },
    error: function(XMLHttpRequest, status, errorThrown) {
    alert(errorThrown);
    }
    });

    • Hi kishor,

      Is the page you are making the Ajax request from a different domain than the domain the request is being sent to? Even if it is a subdomain, IE 9 and below will not let you do a cross domain request using the normal JQuery Ajax.

      You’ll need to do a feature detect and use the XDomainRequest method when necessary.

  3. Instead of worrying about XDomainRequest, use this polyfill http://jpillora.com/xhook/example/ie-8-9-cors-polyfill.html and then you can just use any library ๐Ÿ™‚

    • Libraries are always nice, but it’s important to know whats going on in the background and alternative techniques incase you can’t or don’t want to use libraries. ๐Ÿ™‚

  4. Chiranjib

    That is some data. Thank you Andrew. That really does explain the problems. And yeah, I hate IE8 as well.

  5. Balaji Swaroop

    Firstly thanks for the detailed explanation.

    I tried the same code

    if(window.XDomainRequest){
    var xdr = new XDomainRequest();
    xdr.open(“GET”, “http://updates.html5rocks.com”);
    xdr.onprogress = function () { };
    xdr.ontimeout = function () { };
    xdr.onerror = function () { };
    xdr.onload = function() {
    console.log(xdr.responseText);
    };
    setTimeout(function () {xdr.send();}, 0);
    } else {
    $.ajax({
    type: “GET”,
    url: “http://updates.html5rocks.com”,
    dataType: “html”,
    async:true,
    success: function (response){
    console.log(response);
    }
    });
    }

    This works fine in IE10 but does not work in IE8. Can you please guide me where its wrong.

    Thanks & regards,
    Balaji Swaroop.

    • Hello Balaji,

      I tested your code in IE8 and it seemed to work fine for me:

      http://jsfiddle.net/K6bM6/1/embedded/result/

      Couple of things to check, is the page you are making the call from HTTP or HTTPS?

      The calling page and the destination URL need to both be at the same security level to work in IE8.

      What error are you getting?

      This works in IE10 because IE10 is using the normal AJAX call as it does not implement XDomainRequest.

      Let me know if you need any more assistance.

  6. This is great, and has already helped me quite a bit. However, how to do POST with json in the request body using the IFrame technique?

  7. Wonderful article. I hit a wall and couldn’t get this figured out, even with changing things on the server side it wasn’t working. From first glance this looks like it will work. Thanks again.

    • Glad I could be of help. ๐Ÿ™‚

      Did you get it all figured out now? ๐Ÿ™‚ Feel free to leave another comment if you have any questions!

Leave a Reply