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.
Set-Cookie header tests Netscape-style cookies and the
Set-Cookie2 header tests RFC 2965-style cookies.
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.
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:
- Make sure RFC 2965 cookies is selected and click Reload once. You will see no cookie being sent, but the
Cookie2tells 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.
- 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.
Cookie2header to tell the server the browser can handle RFC 2965 cookies.
- 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.
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-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:
- Make sure RFC 2965 cookies and multiple cookies are deselected.
- Set Use path to full and click Reload.
- Set Use path to full with / and click Reload again.
- 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:
- Make sure Send multiple cookies is not selected.
- Set Use domain to full hostname and click Reload.
- Set Use domain to .washington.edu and click Reload.
- 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
short domain, and
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.