On Cookies and the .NET HttpClient

Cookies, a cup of tea, and a laptop

Have you ever needed to integrate with a web server from your own web server, except this target web server required cookies to function? To clarify—no browser involved. No? GOOD! Cookies are intended for use by browsers to track data related to a user’s session, not for server-to-server communication.

That said, I found myself needing to do exactly that in one of the projects I worked on for a client…

Background

Story time—we were replacing a Single Page App (SPA) and its RESTful web API backend with a newer stack that conformed to emerging standards at this organization. Important to note: we had some external dependencies—one of which was a service that provided quotes for the site’s users and was being built alongside the rest of the system.

Flash forward as we near our deadline, and the calculation service team reveals they are significantly delayed! This service formed part of the core functionality and was a showstopper for going live. The team banded together, and an option emerged—why not use the existing app’s backend to perform the required calculation while moving forward with everything else? This way, we could get the new system live and iron out all the other kinks that come with a new system while still providing full functionality to our users!

The Problem

Problem solved, right? Well, not quite. It took some work to align the new site with the old service in terms of data compatibility, but there was another issue—the older backend was designed solely for browser communication from its website and relied heavily on cookies.

On Cookies

As per MDN, “A cookie (also known as a web cookie or browser cookie) is a small piece of data a server sends to a user’s web browser.” They are not intended for use in backend, server-to-server communication workloads.

The crux of the issue lay in the implicit flow of information—on the original site, each user’s browser “client” had its own set of cookies, allowing the backend to act appropriately. In the proposed solution, all traffic from the browser would first flow into a new backend server, and from there, it would request the quote from the old server. In other words, there was only one “client.”

The calculation service relied on cookie information to correctly identify the user and provide personalized calculation results. This threw a big wrench in the works.

Solution Attempt #1

A high level design of the attempted solution

No big deal, you might think—we can likely mimic the flow of cookies with code! And you’d be right, for the most part.

In this scenario, I was working on a .NET project (.NET 6 at the time) and, of course, using the standard HttpClient abstraction provided by the framework. The HttpClient abstraction, by default, manages cookies for you, automatically retrieving and sending them in subsequent requests. If you use a brand-new HttpClient instance per request, you may never encounter the issue I had.

However, in .NET, when using an HttpClientFactory to create instances of HttpClient, it “pools” the clients to reuse underlying TCP connections, improve performance, and possibly help mitigate port exhaustion.

Now, the astute reader may notice that the default cookie behaviour and connection pooling together could lead to some unexpected behavior. Because I did not reconstruct an HttpClient for each quote calculation request, there was a high chance of a client being reused with another user’s cookies. This happened frequently during early testing (luckily, then and not in production!), so I had to find a solution.

Final Solution

Fortunately, when configuring the HttpClient during dependency injection setup, you can disable cookie handling altogether:

services.AddHttpClient<IMyService, MyService>()
       .ConfigureHttpClient(client => client.BaseAddress = new Uri(Settings.MyServiceUrl))
       .ConfigurePrimaryHttpMessageHandler(() =>
       {
           return new SocketsHttpHandler()
           {
               UseCookies = false
           };
       });

However, by doing so, you must set and read the cookie values yourself. Expect cookies to be returned via the Set-Cookie header—multiple cookies would be sent back as multiple Set-Cookie headers, assuming standard practices are followed.

When sending those cookies back to the target server, use the Cookie header. In this case, you send multiple cookies in a single header value, separated by a semicolon and space. I won’t go into too much detail on the exact format, as you can find plenty of examples of cookies in the wild.

Notable Cookie

It’s worth noting that web servers are often behind load balancers that also serve cookies. These cookies can facilitate “sticky” sessions, ensuring your client returns to the same target server. This can be important, especially in authenticated scenarios, as you want to ensure the same behaviour is followed in the server-to-server scenario.

Conclusion?

Don’t do this. Avoid it at all costs. This is bad practice, and I’m sure many things can go wrong. However, if you—like me—find yourself in a situation where there is no other solution, I hope this helps!