Mark Gilbert's Blog

Science and technology, served light and fluffy.

One too many cookies – Visual Studio and FireCookie

My most recent project held most of the information tied to the current user’s session in cookies.  I needed to be able to pass information among the client logic, the server logic, and the Flash application hosted on the site.  Cookies seemed to be the common and easiest medium for that.  For the most part, that architectural decision turned out to be a good one, with a couple of exceptions.

The exceptions are the topic of this blog post and – of course – most of the issues I’ll describe were simply me learning how the world worked.

For the purposes of today’s post, I’ll use “Flash” to refer to the Flash application hosted in the site, “Client” to represent the client-side JavaScript logic, and “Server” to represent the ASP.NET MVC 2 server application.

 

Taking the red pill

Flash was hosted on the home page of the site, and we wanted it to display a special background image when the user visited the home page in a certain way.  If you visited the default home page, “/Home.aspx/Index”, Client would set a cookie called “background” that contained a default value, but only if the “background” cookie wasn’t already set by the Server.  Whatever value ended up in the cookie, Flash would see it and swap in the corresponding background image.  If, however, you visited one of the “themed” home pages, such as "/Home.aspx/Paper”, then Server would set the “background” cookie, thus preempting Client.  Once this cookie was set, as long as the user didn’t browse to a different themed home page, this cookie would persist during the session, and every subsequent request to the home page would have that background.

At least, that was the theory.  During some of our initial testing, we found that Flash was having problems displaying the correct background every time.  We eventually tracked it down to the value of the “background” cookie.

I ran the site through Visual Studio so I could step through the Server logic to see what was happening.  On the first request to /Home.aspx/Paper, the Server logic would set the “background” cookie in the Response.  The view would then render.  Then, on the next post back a second “background” cookie would appear in Request.Cookies. 

Excuse me?

Oh, and it got worse.  If that second request was for another themed page, my Server logic would add a new cookie to Response (as I had expected), but a copy of that same cookie would also be added to the Request.Cookies collection

What. The. Heck?!?

 

Down the rabbit hole

My first thought at this point was that for some reason, Server was not able to overwrite the cookies being set either by itself or by Client.  I spent several hours trying different methods of adding cookies before I finally came across a StackOverflow.com article that referenced the MSDN documentation on cookies:

ASP.NET includes two intrinsic cookie collections. The collection accessed through the Cookies collection of HttpRequest contains cookies transmitted by the client to the server in the Cookie header. The collection accessed through the Cookies collection of HttpResponse contains new cookies created on the server and transmitted to the client in the Set-Cookie header.

After you add a cookie by using the HttpResponse.Cookies collection, the cookie is immediately available in the HttpRequest.Cookies collection, even if the response has not been sent to the client.

(Emphasis mine; source: http://msdn.microsoft.com/en-us/library/system.web.httpresponse.cookies.aspx)

Ok, that at least explains why cookies were showing up in both Response.Cookies and Request.Cookies. 

With that past me, I turned my attention to why Request.Cookies still had two – one set by Client and one set by Server.  Through a lot more experimentation, I found that my Server cookies were, in fact, being overwritten, but in a round-about way.

Let’s get to the code.  To reproduce this I created an empty MVC 2 application, added a HomeController:

Public Class HomeController
    Inherits System.Web.Mvc.Controller

    Function Index() As ActionResult
        Return View()
    End Function

    <HttpPost()> _
    Function Index(ByVal SubmitButton As String) As ActionResult
        Dim ServerCookie As HttpCookie

        ServerCookie = New HttpCookie("background", String.Format("server-cookie-here: {0}", Now.Ticks))
        HttpContext.Response.Cookies.Add(ServerCookie)

        Return View()
    End Function

End Class

And the Home.aspx/Index view:

<%@ Page Language="VB" Inherits="System.Web.Mvc.ViewPage" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Index</title>
</head>
<body>
    <script type="text/javascript">
        function setCookie(c_name, value, exdays) {
            var exdate = new Date();
            exdate.setDate(exdate.getDate() + exdays);
            var c_value = escape(value) + ((exdays == null) ? "" : "; expires=" + exdate.toUTCString());
            document.cookie = c_name + "=" + c_value;
        }

        function getCookie(c_name) {
            var i, x, y, ARRcookies = document.cookie.split(";");
            for (i = 0; i < ARRcookies.length; i++) {
                x = ARRcookies[i].substr(0, ARRcookies[i].indexOf("="));
                y = ARRcookies[i].substr(ARRcookies[i].indexOf("=") + 1);
                x = x.replace(/^\s+|\s+$/g, "");
                if (x == c_name) {
                    return unescape(y);
                }
            }
        }

        if (getCookie("background") == null) {
            setCookie("background", "client-cookie-here", null);
            document.write("Client cookie set!");
        }
    </script>

    <div>
        <%Html.BeginForm()%>
            Click here to post back: <input type="submit" value="Submit" id="SubmitButton" />
        <%Html.EndForm()%>
    </div>
</body>
</html>

The setCookie and getCookie JavaScript functions used for this demo were pulled from http://www.w3schools.com/js/js_cookies.asp, and were NOT the actual methods I was using when I found this problem (we have a custom library for managing cookies).  However, the source problem ended up being unrelated to the specific Client cookie library.

Finally, I hacked the RegisterRoutes() function in Global.asax.vb to allow for this page to be served as Home.aspx/Index, which was the default home page for the site (again, this is an artifact of this sample which has no real bearing on the problem being analyzed; I include it here just for completeness):

Shared Sub RegisterRoutes(ByVal routes As RouteCollection)
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}")

    routes.MapRoute( _
        "Default", _
        "{*pathInfo}", _
        New With {.controller = "Home", .action = "Index", .id = UrlParameter.Optional} _
    )

    routes.MapRoute( _
        "CatchAll", _
        "{controller}/{action}/{id}", _
        New With {.controller = "Home", .action = "Index", .id = UrlParameter.Optional} _
    )

End Sub

This is enough logic to reproduce the error I was seeing.  I set a break point on the POST version of HomeController.Index(), and inspected the cookie collections:

image

Ignore the third and fourth watches – those will come into play later.  Notice that there are zero Response cookies set so far, and the only Request cookie is from the Client.  The Client cookie was set when the GET version of Index was executed.  So far so good.

Now, if I step through the rest of the Index action method, the Server cookie is added to both collections:

image

Both the Response and Request collections get the new Server cookie.

But why wasn’t the Client cookie being overwritten by the Server cookie in the Request collection?  Why are there two?  If I let the form make another round trip to the Client, things begin to get really hairy:

image

Here we have the form being posted back, but the new Server cookie hasn’t been set yet.  Notice that the Response collection is, again, zero, but there are still two cookies in Request.  If I step through the action method, that’s where the hair-pulling REALLY began:

image

My Response collection looks fine.

My Request collection, however, is just getting out of hand.  Not only do I have both the Client and the Server cookies, I have TWO Server cookies!

At this point, I was convinced that my logic for overwriting cookies was simply not working.  But, as I said before, those hours of research turned up nothing.  If I had just pressed on, I would have saved myself a bit of sanity.  When I let the form make yet another round trip to the Client, I get this:

image

A couple of things to notice here.  The first is that my Request.Cookies collection is back down to 2 cookies – one Client and one Server.  Awesome!  Also notice that the Server cookie returned has the later of the two timestamps shown for the Server cookies in the previous screenshot.  In other words, my Server logic is, in fact, overwriting the Server cookies.  Doubly-awesome!

Ok, so one puzzle solved.  Now I just have to figure out why my Server cookies are still not overwriting the Client one.

For this, I had to dig way, WAY back in my brain, back to my early days of working with the web.  I recalled that there were some other options when creating cookies that would allow them to be read or hidden from portions of your application.  Cookies were tied to the domain that created them, but even within the domain you could separate them out by a property called “path”.  Perhaps the path for these two cookies wasn’t identical, so the browser was treating them as separate creatures.  I checked the property in Studio:

image

Nuts.  Both cookies have the same path.  Well, it was a good try.  I examined the other properties on the two cookies, and couldn’t find anything else that should have been differentiating them.

By this point, I had been falling down this rabbit hole for over a day.  While I had made some clear progress at understanding how cookies were handled, I still hadn’t solved the core issue.  I decided to get another pair of eyes on this issue.  My colleague, Ron, obliged.

His first thought was to pull the site up in Firefox, and turn FireCookie loose on it.  He, too, wanted to examine the cookies, but came at it from a completely different angle than I had.  That turned out to make all the difference in the world.  Here’s what these two cookies looked like in FireCookie:

image

You’ve GOT to be kidding me.  The Path properties WERE different after all.  Visual Studio wasn’t reporting the Client cookie’s Path correctly!

Armed with that information, the solution was easy.  I simply modified the Client cookie logic to explicitly set the Path property to “/”:

function setCookie(c_name, value, exdays) {
   var exdate = new Date();
   exdate.setDate(exdate.getDate() + exdays);
   var c_value = escape(value) + ((exdays == null) ? "" : "; expires=" + exdate.toUTCString()) + "; path=/";
   document.cookie = c_name + "=" + c_value;
}

Notice the addition of “path=/” to the end of the c_value variable. Once I did that, my Client and Server “background” cookies could now be viewed as one and the same. That allowed them to overwrite, and preempt each other.

 

Emerging from the rabbit hole

Beyond learning how the Request and Response cookie collections work, I learned a couple of valuable lessons here, both regarding the Visual Studio debugger.  The first is that is that a round-trip to the browser is apparently required for the debugger to de-dup the list of cookies shown in the Request.Cookies collection.  That was terribly confusing for me.  I was expecting to see the new Server cookie simply overwrite the old one – without having to go down to the browser to do it.

The other lesson learned is that I can’t trust the debugger to accurately report the “Path” property for cookies (whether it can’t report the correct Path for some reason, or it is simply a bug, it’s confusing either way).  I need to use a client-side tool like FireCookie to do that.

Advertisements

July 22, 2011 - Posted by | ASP.NET MVC, Visual Studio/.NET

Sorry, the comment form is closed at this time.

%d bloggers like this: