The best API doesn’t remember past talks or actions (it’s stateless), so it can handle many requests at once without problems.
But what if the server remembers these past actions?
Usually, you might use a DelegatingHandler to manage the session cookie before adding it to the HttpClient. This way needs extra coding to handle when sessions end and other similar things.
An easier way is to use the HttpClientHandler, along with a way to keep things in sync that works well with coding that does many tasks at once.
Below it’s fully implemented client handler that requests the session cookie before sending the request.
public class CookieAwareHttpClientHandler : HttpClientHandler
{
private readonly SemaphoreSlim _semaphore = new(1, 1);
public CookieAwareHttpClientHandler(byte[] certificate, string password)
{
CookieContainer = new System.Net.CookieContainer();
UseCookies = true;
AutomaticDecompression = System.Net.DecompressionMethods.Deflate | System.Net.DecompressionMethods.GZip;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request.RequestUri);
ArgumentNullException.ThrowIfNull(request.Headers.Referrer);
if (CookieContainer.Count == 0)
{
await _semaphore.WaitAsync(cancellationToken);
try
{
if (CookieContainer.Count == 0)
await InitializeCookie(request.Headers.Referrer, cancellationToken);
}
finally
{
_semaphore.Release();
}
}
return await base.SendAsync(request, cancellationToken);
}
private async Task InitializeCookie(Uri? requestUri, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(requestUri);
var initialRequest = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = requestUri,
};
using var response = await base.SendAsync(initialRequest, cancellationToken);
await SeapWebsiteInMaintenanceHandler.EnsureNotInMaintenance(response, cancellationToken);
response.EnsureSuccessStatusCode();
var cookie = CookieContainer.GetCookieHeader(new Uri(requestUri.AbsoluteUri, UriKind.Absolute));
if (string.IsNullOrEmpty(cookie))
throw new InvalidOperationException("Cookie session was not found");
}
}
While you can have many DelegatingHandler, you can only have one HttpClientHandler. This means you can’t use the same HttpClientHandler for different purposes, but you can use the same HttpClientHandler for different HttpClient instances.
Hence, you will add to an ASP.NET core, like this:
services
.AddHttpClient<SeapPublicCommunicationChannel>(client =>
{
client.Timeout = TimeSpan.FromMinutes(10); // we increase the timeout due to the retry handler
client.BaseAddress = new Uri("https://website");
client.DefaultRequestHeaders.Referrer = new Uri("https://website/page");
})
.ConfigurePrimaryHttpMessageHandler(() => new CookieAwareHttpClientHandler())
Conclusions
Implementing cookie management is an infrastructure concurrency. By implementing this in a separate class, you can ensure that the HttpClient is not affected by the session cookie. Moreover you also respect the S from the SOLID principles, the Single Responsibility Principle.