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:
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.
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
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
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…
Set-Cookie header with a library finally solved the problem I had.
- A server sends multiple
Set-Cookieheaders where each header contains a cookie with additional attributes, split by
- A server expects a single
Cookieheader where each cookie is represented via
name=valuesplit by a
- For the love of all that is dear to you, use some libraries for dealing with the
Set-Cookieheader. I ended up using cookie and (more importantly) set-cookie-parser.