Getting Error Messages and Status Codes from Typed HttpClients

Full source code here.

I have been using HttpClientFactory for a while and have generally preferred using named clients over typed clients. With a named client you get the HttpClient just before you are going to use it, and you have full access to the HttpResponse at the point you make the call to the remote service.

var httpClient = _httpClientFactory.CreateClient("InvoiceApi");
HttpResponseMessage response = await httpClient.GetAsync(requestEndpoint);

This is great when things go wrong because you can check the HttpStatus code and the ReasonPhrase, any message, and then take appropriate action.

If you are using a typed client, it might look something like this –

public class InvoiceClient
{
	private readonly HttpClient _client;

	public InvoiceClient(HttpClient client)
	{
		_client = client;
	}
	
	public async Task<Invoice> GetInvoice(int orderId)
	{
		var response = await _client.GetAsync($"invoice/{orderId}");
		Invoice invoice = await response.Content.ReadAsAsync<Invoice>();
		return invoice;
	}
}

And then you use it –

var invoice = await invoiceClient.GetOrder(id);

If the request worked, you are fine, but if it didn’t you and got an error, then the deseriaization will fail and an exception will be thrown, but that exception won’t tell you very much about the cause of the original failure.

Returning a typed response
Here’s the alternative. I use a typed response that includes the HttpStatusCode, the ReasonPhrase and the content I actually want, in this case the Invoice.

public class TypedResponse<T>
{
	public T Content { get; set; }
	public HttpStatusCode HttpStatusCode { get; set; }
	public string ReasonPhrase { get; set; }
	public string Message { get; set; }
}

Here’s how to change the Get method in the Invoice.

[HttpGet("{orderId}")]
public async Task<ActionResult> Get(int orderId)
{
	// Pretend to load the order from the db. 
	Order order = new Order { OrderId = orderId, OrderDate = new DateTime(2018, 6, 14), CustomerId = 1001 };

	var invoiceResponse = await _invoiceClient.GetInvoice(orderId);

	if (invoiceResponse.Content != null)
	{
		order.Cost = invoiceResponse.Content.Cost;
		return Ok(order);
	}
	return StatusCode((int)invoiceResponse.HttpStatusCode, invoiceResponse.Message);
}

And finally, how to use it in the controller.

var invoiceAndStatus = await _invoiceClient.GetInvoice(orderId);

if (invoiceAndStatus.Content != null)
{
    order.Cost = invoiceAndStatus.Content.Cost;
    return Ok(order);
}
return StatusCode((int)invoiceAndStatus.HttpStatusCode, invoiceAndStatus.Message);

Run the code, it will open your browser to http://localhost:5000/api/order/2. The OrderController will call the InvoiceController, the first request will fail, but you will see the error message from the InvoiceController.

Hit F5 in the browser to make another request to the OrderController, again this calls the InvoiceController, but this time the response is a success.

Full source code here.

Selectively Caching Values Inside HttpResponseMessage with Polly – caching series 3/3

Full source code here.

This is the last of three posts on caching with Polly. The first showed how to selectively cache HttpResponseMessages based on the status code of the response.

The second showed how to cache values inside the HttpResponseMessage rather than the whole of the response.

This final post will show how to selectively cache values inside the response based on the status of the response.

If you have read the previous posts the content of this will feel familiar.

Changes to Startup
Like the previous post, add a memory cache, a policy registry and a HttpClientFactory to the service collection.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMemoryCache();
    services.AddSingleton<IAsyncCacheProvider, MemoryCacheProvider>();

    IPolicyRegistry<string> registry = services.AddPolicyRegistry();

    services.AddHttpClient("RemoteServer", client =>
    {
        client.BaseAddress = new Uri("http://localhost:5000/api/");
        client.DefaultRequestHeaders.Add("Accept", "application/json");
    });

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

What’s new is in the Configure method, fist step is to add the registry to the method parameters, DI will take care of this for us.

public void Configure(IApplicationBuilder app, IHostingEnvironment env,
    IAsyncCacheProvider cacheProvider, IPolicyRegistry<string> registry)
{

Next is the cache filter, this one examines the tuple of int and HttpStatusCode and, if the HttpStatusCode is an OK the tuple will be cached for the duration specified. If the respones is not OK the filter will set the cache time to 0 which is interpreted as do not cache by the cache policy.

Func<Context, (int value, HttpStatusCode StatusCode), Ttl> cacheOnly200OkFilter =
	(context, result) => new Ttl(
		timeSpan: result.StatusCode == HttpStatusCode.OK ? TimeSpan.FromSeconds(5) : TimeSpan.Zero,
		slidingExpiration: true
	);

And this is the cache policy that uses the filter above to store the tuple.

IAsyncPolicy<(int, HttpStatusCode)> cachePolicy =
	Policy.CacheAsync<(int, HttpStatusCode)>(
		cacheProvider.AsyncFor<(int, HttpStatusCode)>(), 
		new ResultTtl<(int, HttpStatusCode)>(cacheOnly200OkFilter),
		onCacheError: null);

Lastly, add the policy to the registry.

registry.Add("cacheOnly200OkFilter", cachePolicy);

Changes to the Controller
Over in the CatalogController there many changes.

At the top I take in the HttpClientFactory and policy registry.

private readonly IHttpClientFactory _httpClientFactory;
private readonly IPolicyRegistry<string> _policyRegistry;

public CatalogController(IHttpClientFactory httpClientFactory, IPolicyRegistry<string> policyRegistry)
{
	_httpClientFactory = httpClientFactory;
	_policyRegistry = policyRegistry;
}

Inside the Get method, I setup grab the HttpClient from the HttpClientFactory, set the endpoint of the remote service, take the cache policy from the registry and setup the context for the cache policy to cache based on the incoming request. All of this is similar to normal usage of the Polly cache.

[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
    string requestEndpoint = $"inventory/{id}";
    var httpClient = _httpClientFactory.CreateClient("RemoteServer");

    var cachePolicy = _policyRegistry.Get<CachePolicy<(int, HttpStatusCode)>>("cacheOnly200Ok");
    Context policyExecutionContext = new Context($"GetInventoryById-{id}");	

As in the previous post I don’t call the HttpClient from inside the cache policy, instead I need to call something that returns the int and the HttpStatusCode. I use a local function that makes the request to the remote service, checks the response, deserializes the value and returns a tuple.

(int ItemsInStock, HttpStatusCode StatusCode) itemsInStockAndStatusCode =
    await cachePolicy.ExecuteAsync(context => MakeRequest(), policyExecutionContext);

// local function
async Task<(int count, HttpStatusCode statusCode)> MakeRequest()
{
    HttpResponseMessage response = await httpClient.GetAsync(requestEndpoint);

    if (response.IsSuccessStatusCode)
    {
        int itemsInStock = JsonConvert.DeserializeObject<int>(await response.Content.ReadAsStringAsync());
        return (itemsInStock, response.StatusCode);
    }

    return (0, response.StatusCode);
}

When the cachePolicy is executed it first checks if there is value already in the cache, if there is, that will be returned and the local function will not be called. If there is no value in the cache, the local function is called, if the response of the local function is in the 200 range, the value is stored in the cache.

At this point, the action method has not returned anything to its caller.

Below the local function I check the tuple returned from the call to the cache policy and return a response.

if (itemsInStockAndStatusCode.StatusCode != HttpStatusCode.OK)
{
     return StatusCode((int)itemsInStockAndStatusCode.StatusCode);
}
return Ok(itemsInStockAndStatusCode.ItemsInStock);

For clarity, here is the full Get method.

[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
    string requestEndpoint = $"inventory/{id}";
    var httpClient = _httpClientFactory.CreateClient("RemoteServer");

    var cachePolicy = _policyRegistry.Get<CachePolicy<(int, HttpStatusCode)>>("cacheOnly200Ok");
    Context policyExecutionContext = new Context($"GetInventoryById-{id}");

    (int ItemsInStock, HttpStatusCode StatusCode) itemsInStockAndStatusCode =
        await cachePolicy.ExecuteAsync(context => MakeRequest(), policyExecutionContext);

    async Task<(int count, HttpStatusCode statusCode)> MakeRequest()
    {
        HttpResponseMessage response = await httpClient.GetAsync(requestEndpoint);

        if (response.IsSuccessStatusCode)
        {
            int itemsInStock = JsonConvert.DeserializeObject<int>(await response.Content.ReadAsSt
            return (itemsInStock, response.StatusCode);
        }

        // it doesn't matter what int you use here, it won't be cached as the StatusCode is not a
        return (0, response.StatusCode);
    }

    if (itemsInStockAndStatusCode.StatusCode != HttpStatusCode.OK)
    {
         return StatusCode((int)itemsInStockAndStatusCode.StatusCode);
    }
    return Ok(itemsInStockAndStatusCode.ItemsInStock);
}

Here endeth the series on caching.

Full source code here.