• Web Apps 19.06.2009

    A co-worker mentioned one day that he was having problems with setting multiple cookies in the same Set-Cookie HTTP header, but things were fine if they were set with separate headers. He noted that it was not consistent across browsers, and that the specs seem to indicate that you can set multiple cookies with a single Set-Cookie header; RFC 2109 confirms that.

    However, that RFC has been superseded by RFC 2965, which specifies that the header Set-Cookie2 be used rather than Set-Cookie. If you look at the specs for Netscape cookie behavior you’ll see it specifies using Set-Cookie but only shows one cookie being set per header.

    I hadn’t seen Set-Cookie2 in use before, and thought it would be a good topic to investigate. I work quite a bit with Apache and took a look at its source; Set-Cookie2 is referenced in a couple modules, but for the most part Apache will just pass cookie headers as-is. Modules which explicitly set cookies (such as mod_rewrite) will use Netscape-style cookies.

    This post will not cover the specifics of how to write code to manipulate cookies. There are several good resources explaining these techniques, as well as packages which handle the gory details. The purpose of this post is to explain cookie behavior.

    Cookie testing example

    This example shows what different browsers do when cookies are set. It is implemented in an iframe to simplify reloading it several times. I realize that it’s cumbersome to constantly scroll between the cookie tester and the text which explains how to use it; one way to make it easier to use is to have this post opened in two windows, one reading the explanation and the other running the tests.

    When you click on the Reload button, the frame will be reloaded with the selected settings. Clicking on the HTTP Headers also reloads the frame but it causes the script to modify the headers to clear cookies from the browser which match the current settings (such as the path). Clicking on the Javascript button will not cause a reload, but will force the browser to use Javascript to clear all cookies.

    You will see three sections: what the browser sent to the server, what the server sent back to the browser, and what cookies Javascript detected (you may not see anything in this last section if you are using IE 6; this test does not attempt to work correctly with that browser).In the server section, the Set-Cookie header tests Netscape-style cookies and the Set-Cookie2 header tests RFC 2965-style cookies.

    The output can be a bit confusing since the sent by browser section reflects the cookies which were set by the previous request. The detected by Javascript section, however, should show the cookies previously stored as well as those the browser processed from the sent by server section.

    Note there may be other cookies sent to the test script (such as for Google Analytics), but cookies not relevant to this example are filtered out.

    The different options you can choose will be explained below the test, where I discuss how the options influence the results.

    • Clear cookies via:

    RFC 2965 cookie support

    Of the major browsers (latest versions at the time of this writing) only Opera supports RFC 2965 cookies. You can see this is the case because the test script sends two different cookies named TestCookie: one set by Set-Cookie and the other set by Set-Cookie2. Following the RFC, Opera ignores the Set-Cookie version; the value of that cookie contains RFC 2965 Cookie with Opera, but contains Netscape on the other browsers.

    If you uncheck the box so RFC 2965 cookies are not sent back to the browser, Opera will send a Cookie2 header to indicate which version of cookies it supports.

    Opera can send a mix of Netscape and RFC 2965 cookies in the same header, but if any RFC 2965 cookies are being sent, the $Version value will be included. To see this behavior in Opera:

    1. Click on Javascript to clear all test cookies.
    2. Make sure RFC 2965 cookies is selected and click Reload once. You will see no cookie being sent, but the Cookie2 tells the server that RFC 2965 cookies will be accepted. Click Reload again and you should see a cookie being sent by the browser with a version number, following RFC 2965.
    3. Deselect RFC 2965 cookies, change Use path to /, then click Reload twice. You’ll see the non-RFC 2965 cookie is being sent along with the RFC 2965 cookie, and the header has the version string.
    4. Click on Javascript to clear all the test cookies again. Click on Reload twice and this time you will see the cookie being sent with the Cookie2 header to tell the server the browser can handle RFC 2965 cookies.
    5. Re-select RFC 2965 cookies and change Use path to none, then click Reload twice. You will see that the cookies will be sent with the version string.

    Because of this behavior, you need to be careful if any other application sets cookies which are scoped such that they will also be sent to your application, since they may be set using a different spec. Fortunately, the format will probably be close enough, but beware that the two standards have different quoting mechanisms (discussed below). Opera will not put quotes around the value of Netscape-style cookies, so you can’t assume all the cookies will be properly formatted for RFC 2965.

    Setting multiple cookies in one header

    The question which prompted this post was about setting multiple cookies in one Set-Cookie header. As mentioned above, the Netscape specification only indicates that a single cookie can be set. Even so, Safari will accept a comma-separated list of cookies and set all of them. Other browsers will only set the first cookie and ignore the second one.

    If you select Send multiple cookies while leaving Separate multiple Netscape cookies set to comma and click Reload, you’ll see that the detected by Javascript section shows TestCookie and TestCookie2 on separate lines. On all other browsers, these two cookies will appear on the same line, indicating that the whole line is the actual value of the cookie. You can verify this by examining the cookies in each browser’s preferences.

    If you next change Separate multiple Netscape cookies to semicolon, all browsers will have the same behavior. Each will show only one cookie and the value will be what you’d expect (i.e. not also include the string for the second cookie).

    Differing quoting mechanisms

    The Netscape cookie standards explicitly states that the cookie value is

    … a sequence of characters excluding semicolon, comma and white space.

    You need to use URL-style encoding for special characters (e.g. %2c for a comma). You also cannot use quotes around the cookie’s path or around any expiration date.

    Values for RFC 2965 cookies can be either quoted strings or tokens, the latter which is essentially non-special, non-whitespace characters. This means you need to be careful if you’re sending both Set-Cookie and Set-Cookie2 headers, since they need to be quoted differently.

    You may have noticed that in the test for the previous section, browsers other than Safari allow a cookie to contain a non-quoted comma, contrary to the Netscape specification.

    Same cookie, different paths

    Both Netscape and RFC 2965 cookies allow you to set a path to scope the cookie, as well as setting a domain. If you send the same cookie multiple times but with different paths, the browser will send all of the cookies, and cookies with the narrower scope will appear before others having a broader scope. For example, if you send:

    Set-Cookie: Test=value1; path=/a/b
    Set-Cookie: Test=value2; path=/a/b/c

    The browser will send back:

    Cookie: Test=value2; Test=value1

    Default path for Netscape cookies

    If you do not specify the path, Netscape cookies will default to the

    path of the document that created the cookie property

    which seems ambiguous. Indeed, the different browsers seem to treat the default differently. To examine the behavior:

    1. Clear cookies by clicking the Javascript button.
    2. Make sure RFC 2965 cookies and multiple cookies are deselected.
    3. Set Use path to full and click Reload.
    4. Set Use path to full with / and click Reload again.
    5. Set Use path to none and click Reload twice.

    After doing this, Safari will send the cookie full path with / and no path. This would indicate that Safari treats the lack of the path as being the same as the path up to but not including the last /. Since the path with the trailing / is more specific, that cookie gets sent before the one without the path.

    Firefox and IE will send the cookie no path followed by full path, indicating that they treat the lack of the path as including the trailing /.

    Opera seems to treat the path the same, whether or not it has a trailing /. You can see this because it will only send the no path cookie. In fact, you should have seen that no more than one cookie is being sent during all steps.

    Default path for RFC 2965 cookies

    RFC 2965 is more explicit about what should happen if a cookie has no path. It says that the default path is the same as the URL of the request

    up to and including the right-most /.

    You can test Opera’s RFC 2965 cookie behavior by running the same test as above but make sure the RFC 2965 checkbox is selected. As of this writing, the latest version (9.64) seems to not follow the behavior specified by the RFC. Instead, it has the same behavior as Netscape cookies, treating the paths with and without the trailing / the same.

    Same cookie, different domains

    Just as with paths, if the same cookie is set with different domains, they will all get sent to the server. Both the Netscape and RFC 2965 specifications state that the default domain should match the hostname in the request. Unlike the path, however, cookies with the same name but different domains have no specified order in which they should be sent. This can be tested with:

    1. Clear cookies by clicking Javascript.
    2. Make sure Send multiple cookies is not selected.
    3. Set Use domain to full hostname and click Reload.
    4. Set Use domain to .washington.edu and click Reload.
    5. Set Use domain to none and click Reload twice.

    You can see that Safari will send three cookies, the first one having short domain, the second full domain, and the third no domain. Firefox also sends three cookies, but in the order of full domain, short domain, and no domain.

    Opera and IE will assume the lack of a domain is the same as setting the full domain, which is why you only see the cookie without a domain and the one with short domain. Both of these browsers also seem to order the cookies from most-specific domain (or with no domain) to least-specific.

    Cookie date formats

    The Netscape specification says that dates used for cookie expiration should use two digits for the year, such as:

    Thu, 01-Jan-70 00:00:00 GMT

    Safari, Firefox, and Opera accept two-digit years, but IE requires four digits. Fortunately, the other browsers also accept four digits.

    RFC 2965 gets around that by stating a cookie’s lifetime should be specified with Max-Age which specifies the number of seconds the cookie should live. A value of zero specifies that a cookie should (but not must) be deleted immediately. Opera does remove cookies in this case.

    Posted by fmf @ 5:39pm

  • Leave a Reply