Sunday, September 7, 2008

On The Care and Handling of Cookies

By Paul Riley

Everything you ever wanted to know about ASP.NET Cookies but were too afraid to ask.
Introduction
What exactly is a cookie anyway? According to Websters Online, a cookie is any one of the following:

cookie : a small file or part of a file stored on a World Wide Web user's computer, created and subsequently read by a Web site server, and containing personal information (as a user identification code, customized preferences, or a record of pages visited)
As tempting as the other definitions may be, what we're looking at here is the third. A cookie is a small packet of information sent as part of the HTTP Response, stored on the client machine and subsequently sent as part of any HTTP Request to the originating web site. In ASP.NET terms, a page receives a CookieCollection as a property of an HttpRequest object (usually Request.Cookies) and returns a CookieCollection of updates as a property of an HttpResponse object (usually Response.Cookies).

Cookies are generally used for various levels of state management: maybe for keeping a user logged on to a site or for keeping track of the last time a site (or area of a site) was last visited.

I recently used cookies to keep track of a tournament signup process, where a team captain might sign up as many as 10 players to a team, one at a time. I was pretty sure that at some point a user's browser might fall over, or either the client or server machine might crash, or a user might click on another link in the menu. I didn't want them to have to start over again, so I stored a Session ID in a cookie and on each signup record in the database. This Session ID is easily retrieved the next time the user comes back to the signup page and I could pick up all the data from the database and save the user a lot of time.

Cookies are a very powerful tool in web design but in ASP.NET they can also be the cause of many problems, especially for users of ASP (which processes cookies slightly differently). Nothing here is rocket science but it is only simple once you understand what's going on behind the scenes.

Cookie Expiration
The first thing you need to understand about cookies is this: Cookies carry an expiry date. The second thing you need to understand is this: Expiry dates are the cause of most cookie-related bugs.

Every time you set the Value of a cookie, remember also to set the Expires date. If you fail to do this you will quickly find yourself losing Cookies owing to them having expired immediately when updating them on the client machine or when the browser closes.

When a cookie expires, the client no longer sends it to the server, so you need to make sure that the Expires property of the cookie is always in the future. If you just set a cookie's value then it will create a cookie with Expires set to DateTime.MinValue (01-Jan-0001 00:00).

You can set a cookie's Expires property using any DateTime value (a positive relief after ASP). For example, if you want a Cookie to expire after the user has not been to that part of your site for a week, you would set Expires = DateTime.Now.AddDays(7).

If you want the cookie to be permanent then the temptation is to use DateTime.MaxValue, as I did in the lat version of this article. However, there is a simple gotcha here.

DateTime.MaxValue is precisely 31-Dec-9999 25:59:59.9999999. But Netscape, even at version 7, doesn't cope with that value and expires the cookie immediately. Amazingly, and somewhat annoyingly, investigation showed that Netscape 7 will cope with 10-Nov-9999 21:47:44 but will not handle a second higher (I'll be honest, I didn't test it to any fraction of a second, I really wasn't interested).

Thus if, like me, you subscribe to the "it doesn't have to look pretty on Netscape, as long as it's functional on the latest version" school of thought, always use a date prior to that. A commonly accepted "permanent" cookie expiry date is DateTime.Now.AddYears(30), ie. 30 years into the future. If someone hasn't visited your site for that long, do you really care what the state was last time they were there?

Disposing of Stale Cookies
If you want to delete the cookie on the client machine, do not use the obvious Response.Cookies.Remove("MyCookie") which simply tells the cookie not to overwrite the client's cookie (see below for a more detailed explanation), set the cookie's Expires property to any time prior to the current time. This will tell the client to overwrite the current cookie with an expired cookie and the client will never send it back to the server.

Again, the temptation is to use DateTime.MinValue (01-Jan-0001 00:00:00) to delete a cookie; again, this would be a mistake. This time, Netscape 7 will work as expected but Internet Explorer 6 considers this to be an exceptional case. If Internet Explorer receives a cookie with what it considers to be a "blank" expiry date, it will retain the cookie until the browser is closed and then expire it.

This could, of course, be useful if the behaviour was consistent across browsers. Unfortunately that is not the case and trying to use the Internet Explorer functionality will cause the page to fail when viewed in Netscape.

Another easy trap to fall into: in theory, you should be able to use DateTime.Now to immediately expire a cookie but there are some dangers in that.

If the server machine time is not quite syncronised with the client machine time, it's possible that the client will think the time given is somewhere in the (admittedly near) future. This can cause a bug to show up when uploaded to a live server that wasn't obvious when testing locally. Worse, it could create a situation where a web application works fine when you view it but not when another user accesses from his machine.

Both situations are notoriously hard to debug.

The safest (and most symmetric) way to delete the cookie by using an Expiry date of DateTime.Now.AddYears(-30).

Incoming Cookies
When a page is received, it has a CookieCollection inside the Request, listing all the cookies in this namespace on the client machine. Any cookies that do not exist in the Request will be null if you try to access them (so be careful of looking for the Value property unless you are sure it exists).

For the Response, on the other hand, there are no cookies when your code begins, they are created as and when you need them. When the server sends back the Response, the client machine only adjusts those Cookies that exist in the Response.Cookies collection; any others are left alone.

In what seems like a bizarre twist of fate, the incoming (Request) cookie carries an Expires date of DateTime.MinValue, regardless of the date attached to the cookie on the client system.

This is actually quite easily explained - as many web developers know, it's near impossible to get hold of the expiry date of a cookie once it is written to the client machine (try it in JavaScript). It certainly isn't sent as part of the request. But Microsoft will have wanted Response and Request cookies to be of the same class (HttpCookie). As DateTime is a value object, rather than a reference object, it cannot be null so it must have a value. The best arbitrary value would be DateTime.MinValue.

Understandable as it is, this is another place we can get caught out if we are not careful. If we want to copy a Request cookie directly to the Response (something we will later see is a useful tool) then we need to create a new expiry date, even if we can safely assume the old date will be okay.

The Mysterious Case of the Disappearing Cookie
If you try to access a cookie that doesn't exist in the Response.Cookies collection, it will be created with an empty string in the Value and an Expires date of 01-Jan-0001 00:00. Strangely, it also creates a matching cookie in the Request.Cookies collection if one doesn't already exist.

More...
http://www.codeproject.com/KB/aspnet/aspnetcookies.aspx

ASP.Net Feeds