Blog

Home / Thoughts, sketches, dev cases

Loopback mode for HTTP client

Loopback mode for HTTP client

Sunday, April 23, 2023

http client

Comments

Our application has an Azure Function that reacts to the service bus message when particular entity gets created. When it happens, we need to perform configuration of that entity on different services - configure database, put secret into keyvault, make dozens of requests to several APIs and so on. So happened that we do not have proper DEV environment that would cover all the infrastructure — by this I mean environment where we can safely add/update/delete entities without interfereng with the real business. We have QA and PROD, so whenever a new feature is deployed even to QA, it becomes a part of some business processes where dummy data is not really appreciated, not to speak about PROD.

So to be able to proceed with development and do not bother the business much, it was clear that we need some emulation mode for the azure function — I called it "loopback mode". With some simplification it is about when higher application layer would be "loopback-mode agnostic", but lower infrastructure layer would get some flag from application settings and either emulate or perform real HTTP calls to the external APIs. This will let to cover application logic with unit tests smoothly and test its logic on each deploy regardless of the flag.

However, in this case infrasturcture layer would become difficult to be covered with unit tests properly. For each individual HTTP request, we would need to introduce branching and test behavior that is not the part of the application itself. When some requrement will change and expected response will change also, we will need to reflect this in two different places — in the real workflow and in the loopback workflow. Moreover, every method that makes request to external API, needs to deal with the parameter that is not part of the business but the part of launch context. In sum, all this started smelling not really good to me, so I started thinking for some kind of different solution.

It was obvious that if I want to have this loopback mode, somewhere in the code there must be "if" which will route HTTP request to either real external API or to the mocked response. I just wanted to make source code adhere to its intrinsic logic as much as possible and have this feature to influence the unit tests as little as possible. So the solution I've come up with was the http message handlers that can be configured for HttpClient.

Http message handlers is pretty cool feature that can be used for customizing http client behavior without affecting the code which uses that client. Http client from this perspective can be treated as piece of functionality that has its own middleware facility, somewhat like api controllers and their middleware. Http client starts request chain by being called with method Send/SendAsync with configured http message, and downstream the request is served by http message handler which can be either default or custom. If the default message handler which performs real calls outside will be replaced with custom one which will just return some mock responses, the purpose will be served — calling code will remain unaware of the source for the http responses, be it response from real external API or mocked data returned by loopback http message handler. And this is what I implemented in the code.

So, we have three principal parts to implement this approach:

1 - Response mocking code

This is something which can be implemented in many ways, but generally it is a handler which inputs HttpRequestMessage, give it some analysis to understand which exactly endpoint is being called, and returns respective HttpResponseMessage. In my case it is enough to implement it as simple Func delegate:


public static class Loopback
{
    public static readonly Func Handlers = req =>
    {
        // POST geospatial data store
        if (req.Method == HttpMethod.Post)
        {
            if (req.RequestUri!.AbsolutePath == "/DataStores")
            {
                var json =
                    $$"""
                    {
                      "id": "{{Guid.NewGuid()}}",
                      "dataStoreType": "ms",
                      "name": "Operation.loopbackMock",
                      "displayName": "Operation loopback mock"
                    }
                    """;

                return new HttpResponseMessage(HttpStatusCode.OK)
                {
                    Content = new StringContent(json)
                    {
                        Headers = { ContentType = new MediaTypeHeaderValue("application/json") }
                    }
                };
            }
		}
		
		// ..other similar pieces
	}
}

As it is seen, we can response with complex json objects as well as with simple values or just HTTP status codes to properly mock real API response.

2 — Custom message handler, very simple class


public class LoopbackMessageHandler : AbstractLoopbackMessageHandler
{
    public LoopbackMessageHandler(Func handler) : base(handler) { }
}

3 — configuration

in this part we analyze specific configuration key to understand if the application is launched in loopback mode. If so, we configure primary http message handler for http client.


[ExcludeFromCodeCoverage]
public static class BaseHttpClientsDependencies
{
    public static void InitializeBaseHttpClients(this IServiceCollection services, bool loopbackMode = true)
    {
        var client = services.AddHttpClient((sp, c) =>
        {
			// Another Http client to communicate with identity provider and obtain jwt token
			// it does this upon initialization, registered as Scoped dependency, so it is enough
			// to get it from the dependency container to have fresh jwt
            var auth0HttpClient = sp.GetRequiredService();

			// Get some settings from respective Options
            var settings = sp.GetRequiredService>().Value;

			// Set the base address
            c.BaseAddress = new Uri(settings.ApiHost ?? throw new ArgumentNullException(nameof(settings.ApiHost)));

			// Set the bearer token
            c.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", auth0HttpClient.BearerToken);
        });

		// Finally, if loopback mode is set, configure primary http message handler
        if (loopbackMode) client.ConfigurePrimaryHttpMessageHandler(_ => new LoopbackMessageHandler(Loopback.Handlers));
   }
}

And that's basically it. Any method that will have configured dependency from this IBaseHttpClient interface will get class instance to work with and remain unaware if it will send real requests to external APIs or will return mocked responses.

Situations when this approach may be helpful:

  • when developing is being conducted in heterogenous environment where it is good to have some of services excluded from acutal interaction with developed application to not flood them with testing data;
  • ability to put the application in any kind of intergation tests without bothering external API services

Thanks for reading!

© theyur.dev. All Rights Reserved. Designed by HTML Codex