The Set-Cookie and Cookie headers got me confused, but I think I get it now
I learned a thing or two about cookies today… It took me quite a while to figure it out, but I think I understand it now.
I am rebuilding a website from an old PHP / Twig setup into something more modern. The PHP backend is still in use, but I wanted to separate the front-end and rebuild that with Remix. The backend gets slowly transferred into an API only backend that my Remix server is going to communicate with.
Authentication is done via cookies, but since I now have a Remix server between the client and the API, I need to perform the requests to the backend with the session cookies from the user. Having the Remix server in between allows me to do a couple of requests to the API without going all the way back to the client for each response. But that means getting and sending cookies along as well.
The problem I faced was this: When I create a user, I also need to store the country that user lives in. For the user, that is one form to will out: username
, password
, countryCode
. But the backend wants to store the countryCode
via a separate request. So first create the user, then update that user via an other endpoint with the country code. But only an authenticated user can update their country code.
I needed to get the session cookie from the createUser
endpoint, and provide that to the updateCountry
endpoint right after.
Set-Cookie
and Cookie
are two different things
It took me a while before I realised that the server sends a Set-Cookie
header to the browser, but expects a Cookie
header when receiving.
When I figured that out, it was a matter of getting the Set-Cookie
header, and passing it as Cookie
, right? No, that did not work at all.
Looking at the original requests from the legacy codebase, each request to the backend has a Cookie
request header with some information (session=s3ss10n1d
for example). But multiple cookies can be stored for a domain, so the value can also be: session=s3ss10n1d; setting=theme-blue
. Right, a header, cookies split via ;
. I get that.
Looking at the response headers though, there were multiple Set-Cookie
headers. One for each cookie, each of that with the following shape: cookieName=cookieValue; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; domain=.some-site.com; secure; HttpOnly; SameSite=Lax
. Ok, the cookie has a name / value combo and some attributes separated by a ;
. I get that as well.
Reading response headers
The Response contains a headers
property that is an instance of Header. You can use it to query values of response headers via header.get()
. From the MDN documentation:
If the header has multiple values associated with it, the byte string will contain all the values, in the order they were added to the Headers object
The code example with it:
myHeaders.get('Accept-Encoding'); // Returns "deflate, gzip"
myHeaders.get('Accept-Encoding').split(',').map((v) => v.trimStart()); // Returns [ "deflate", "gzip" ]
That does not seem so hard… Let’s have one more close look at the Set-Cookie
value: cookieName=cookieValue; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; domain=.some-site.com; secure; HttpOnly; SameSite=Lax
. Please notice the expires
value: Thu, 01-jan...
. That ,
right there is the sole reason a good amount of hours got wasted. There is no way to tell where to split the string without intimate knowledge of all possible attributes for the cookie…
Parsing the Set-Cookie
header with a library finally solved the problem I had.
Lessons learned
- A server sends multiple
Set-Cookie
headers where each header contains a cookie with additional attributes, split by;
. - A server expects a single
Cookie
header where each cookie is represented vianame=value
split by a;
. - For the love of all that is dear to you, use some libraries for dealing with the
Set-Cookie
header. I ended up using cookie and (more importantly) set-cookie-parser.