Introduction
We have multiple microservices in our stack that calls each other. When an error occurs, it's really nice to be able to correlate all the calls to see where something went wrong. Imagine that you have microservice 1 that calls microservice 2 and something goes wrong in microservice 2. Microservice 1 will then return a status code, usually 500, and the UI will act accordingly and display "Something went wrong, please contact us and provide the following information: abc-123-98765".
In this example, abc-123-98765 is a unique identifier for the request that failed, it's called a Correlation Id.
Eventually, the dev team will get a ticket in JIRA saying that something went wrong, please investigate. The developer will then take the correlation id and begin searching in the logs.
As you can imagine, using correlation ids to correlate (pun highly intended) calls between your applications are really useful and valuable, especially when debugging. That's why it's so important to pass along the correlation id wherever possible.
In our system, the UI will always generate a correlation id and pass it along to our API gateway. We will then take this correlation id and forward it to all the different applications that we integrate with.
Implementations
Let's take a look at how we can do that in dotnet. Our HttpClient currently look like this:
public class DummyHttpClient
{
private readonly HttpClient _httpClient;
public DummyHttpClient(HttpClient httpClient)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
public async Task<string> GetData()
{
var request = new HttpRequestMessage(HttpMethod.Get, "/data");
using var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
As you can see, we are not using any correlation id here.
In the HttpClient itself
public async Task<string> GetData(string? correlationId)
{
var request = new HttpRequestMessage(HttpMethod.Get, "/data");
if (!string.IsNullOrWhiteSpace(correlationId))
{
request.Headers.Add(Headers.CorrelationId, correlationId);
}
using var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
We have now changed the signature of our GetData method, it's now possible to pass in a correlation id. If it has a value, we will add the correlation id header. Great. But it turns out that we need to add some validation before adding the header. If we send a correlation id header longer than 32 chars, the receiving system will return an error.
public async Task<string> GetData(string? correlationId)
{
var request = new HttpRequestMessage(HttpMethod.Get, "/data");
if (!string.IsNullOrWhiteSpace(correlationId) && correlationId.Length <= 32)
{
request.Headers.Add(Headers.CorrelationId, correlationId);
}
using var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
Problem solved.
I have a couple of issues with the above solution though.
- If we add another method, GetData2, we will need to repeat the same logic.
- A correlation id needs to be passed to every method - and possibly also changing the methods signature.
Using a DelegatingHandler
IMO, using a DelegatingHandler is a perfect fit for this use case.
First, let's introduce the following interface. It will be responsible for getting the actual correlation id from the current request.
public interface ICorrelationIdQuery
{
Task<string?> Execute();
}
An implementation using HttpContext can look like this:
public class HttpContextCorrelationIdQuery : ICorrelationIdQuery
{
private const int MaxLength = 32;
private readonly IHttpContextAccessor _httpContextAccessor;
public HttpContextCorrelationIdQuery(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
}
public Task<string?> Execute()
{
if (!_httpContextAccessor.HttpContext.Request.Headers.TryGetValue(
Headers.CorrelationId,
out var headerValues))
{
return Task.FromResult<string>(null!)!;
}
if (string.IsNullOrWhiteSpace(headerValues))
{
return Task.FromResult<string>(null!)!;
}
var correlationId = headerValues.First();
if (correlationId.Length <= MaxLength)
{
return Task.FromResult(correlationId)!;
}
return Task.FromResult<string>(null!)!;
}
}
And the DelegatingHandler looks like this:
public class CorrelationIdDelegatingHandler : DelegatingHandler
{
private readonly ICorrelationIdQuery _correlationIdQuery;
public CorrelationIdDelegatingHandler(ICorrelationIdQuery correlationIdQuery)
{
_correlationIdQuery = correlationIdQuery ?? throw new ArgumentNullException(nameof(correlationIdQuery));
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var correlationId = await _correlationIdQuery.Execute();
if (!string.IsNullOrWhiteSpace(correlationId) && !request.Headers.Contains(Headers.CorrelationId))
{
request.Headers.TryAddWithoutValidation(Headers.CorrelationId, correlationId);
}
return await base.SendAsync(request, cancellationToken);
}
}
Here we are checking the current request (using HttpContext via ICorrelationIdQuery), if it contains a valid correlation id, we will grab that and append it to our outgoing http request.
We can the tell our DummyHttpClient to use this delegating handler like this:
services.AddHttpClient<DummyHttpClient>()
.AddHttpMessageHandler<CorrelationIdDelegatingHandler>()
Now we can remove all code regarding correlation id from our DummyHttpClient:
public async Task<string> GetData()
{
var request = new HttpRequestMessage(HttpMethod.Get, "/data");
using var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
If we add any new methods to DummyHttpClient, we don't need to do anything, the delegating handler will add the header for all requests. And if we add any new HttpClients, the only thing we need to do is to remember to call the .AddHttpMessageHandler extension method like this:
services.AddHttpClient<AnotherHttpClient>()
.AddHttpMessageHandler<CorrelationIdDelegatingHandler>()
The delegating handler can be found here together with a couple of tests.