XMLHttpRequest::setRequestHeader()
I’ve been working on rewriting a bunch of Air Mail lately. Partly because pieces of it (the ones I wrote) aren’t very good but also because I’m using it as an opportunity to reacquaint myself with some more heavy-duty DHTML coding.
One of the things I’ve been focusing on is the concept of having one codebase that works as both a desktop application in Adobe AIR and as a browser-based application served from a web server. This effort is worthy of an entire post on its own, but there is one thing that has been a particular pain in the butt…cookies (this is a recurring theme in my life, it seems).
Air Mail uses the Yahoo! Mail Web Service, which requires cookies to be sent in the request for authentication. In JavaScript, one of your only mechanisms for making web service requests is to use XMLHttpRequest. The XMLHttpRequest object provides a method named setRequestHeader, which allows setting arbitrary headers on HTTP requests. This should, in theory, allow you to set the “Cookie” header.
Theory, however, is often not reality. While this happens to work in Adobe AIR and Firefox, this doesn’t work in Safari, Opera or Internet Explorer (at least not in the versions I tested). Reading the specification for XMLHttpRequest, I don’t see anything indicating that the “Cookie” header ought to be restricted. I can only guess that Safari, Opera and IE are either buggy or decided to take some liberties (possibly in the name of security) when implementing the specification. If you’re interested, I have a test page that shows this issue in action. The page loads and makes an XMLHttpRequest to a page that does just echoes back the cookies it sees. If it works, you should see a cookie named “thisisa” with a value of “test” show up. In my case, I see it in Firefox only.
In any case, this does cause some complications for Air Mail.
Adobe AIR is able to send requests directly to mail.yahooapis.com. The web-based applications, however, have to send requests to ymail.unclehulka.com (the same host the web page is served from) due to the same-site restrictions imposed by the browser sandbox. I have an endpoint running on ymail.unclehulka.com that receives those web service requests and proxies them to mail.yahooapis.com. The code is identical, the only difference is the URL that Air Mail hits.
I’ve managed to configure the URL to connect to for the different application versions using GNU M4, but the cookies have been another problem. I’ve been reluctant to make two separate code paths for the different environments, simply because I think the single codebase is the most compelling feature of Air Mail. At this point I’ll likely move the cookie header name to an M4 configuration template. The desktop Air Mail will use “Cookie” as the header name. The web-based Air Mail will use “X-Cookie” or some other header not named “Cookie”, so XMLHttpRequest doesn’t prevent it from being passed. Then the proxy on ymail.unclehulka.com can simply copy that header to the “Cookie” header before sending the request on to mail.yahooapis.com.
Update: As Steve pointed out in the comments, there is a way around this issue in Internet Explorer…simply call setRequestHeader() twice. As I pointed out in my comment, I’m using the YUI Connection Manager, so I don’t actually control the calls to setRequestHeader() in this case.
November 17th, 2007 at 9:29 am
Your test works in Safari 3.0 on Leopard as well.
November 17th, 2007 at 9:30 am
When I refreshed it though, I see Google Analytics cookies
November 17th, 2007 at 12:50 pm
You need to add it twice via setHeader in IE: http://support.microsoft.com/?id=234486
Or, just use setCookie() instead.
November 17th, 2007 at 10:08 pm
Sam, I’m running 3.0.4 on Leopard and I never see my “thisisa” cookie. What version are you running?
Steve, yeah…I’ve seen that bug called out before. Unfortunately, since I’m using the YUI connection manager, I’m not actually controlling the call to setRequestHeader().
November 19th, 2007 at 5:57 am
Weird… I see it on Safari 2.0.4 (419.3):
Cookies
thisisa=test
Has a workaround been found, Ryan?
November 19th, 2007 at 9:52 pm
Frank…not that I’m aware of. I still don’t see the cookie in 3.0.4. So…odd.
December 8th, 2007 at 1:20 pm
The improper cookie handling is new in Safari 3 (at least in Leopard - I didn’t go back to test in Tiger). It seems that XMLHttpRequest for any Webkit-powered application (this includes the Dashboard) is using Safari’s cookie storage and sends the cookies in Safari. This most likely is the reason why your test works for some (it does for me in 3.0.4/10.5.1) but not for others - if Safari already has cookies for the site the XHR is going to, it the XHR will send those cookies. If no cookies are present, it seems as if the cookies specified in the setRequestHeader() are correctly used.
This is a serious issue as it breaks all cookie handling for Dashboard Widgets - I reported this to Apple as a bug back on 10/29 but it hasn’t even been confirmed yet (rdar://5567386)
The following is a little test to verify what is going on:
Steps to Reproduce:
1. Visit http://www.apple.com at least once with Safari to make sure that Safari has some cookies for the site
2. Create a new HTML file with the content outlines below and open it in Safari
3. Open Terminal and execute the command specified in the first step outlined in the HTML
4. Click the link in the second step of the HTML (this will send a new xmlRequest to http://www.apple.com with explicitly setting the Cookie to “” (i.e., no cookie sent)
5. Check the Terminal for the output of the command - this will (in addition to summary information from the tcpdump command) show the Cookie header sent by the xmlRequest
xmlRequest Cookie Test
function send_xmlRequest()
{
var xmlRequest = new XMLHttpRequest();
xmlRequest.onload = function() {}
xmlRequest.open("GET","http://www.apple.com/");
xmlRequest.setRequestHeader("Cache-Control", "no-cache");
xmlRequest.setRequestHeader("Cookie", "");
xmlRequest.send(null);
}
Type the following command in the Terminal
sudo tcpdump -i en1 -c5 -A -s1500 dst host http://www.apple.com | grep ^Cookie
(using en0 instead of en1 when using a wired instead of wireless connection
Send new xmlRequest with empty Cookie via JavaScript
Check Therminal output
December 8th, 2007 at 1:22 pm
Arrgh, isn’t <code> supposed to prevent this? Trying again…
<html>
<head>
<title>xmlRequest Cookie Test</title>
<script type="text/javascript">
function send_xmlRequest()
{
var xmlRequest = new XMLHttpRequest();
xmlRequest.onload = function() {}
xmlRequest.open("GET","http://www.apple.com/");
xmlRequest.setRequestHeader("Cache-Control", "no-cache");
xmlRequest.setRequestHeader("Cookie", "");
xmlRequest.send(null);
}
</script>
</head>
<body>
<ol>
<li>Type the following command in the Terminal<br />
<tt>sudo tcpdump -i en1 -c5 -A -s1500 dst host http://www.apple.com | grep ^Cookie</tt><br />
(using <tt>en0</tt> instead of <tt>en1</tt> when using a wired instead of wireless connection</li>
<li><a href=”#” onclick=”send_xmlRequest();”>Send new xmlRequest with empty Cookie via JavaScript</a></li>
<li>Check Therminal output</li>
</ol>
</body>
</html>
December 8th, 2007 at 10:05 pm
http://bugs.webkit.org/show_bug.cgi?id=16357#c4
Seems like this is located fairly deep in the system…