Ocelot & files download
Tags: ocelot, dotnet, largefile, gateway
Published: 2023-04-11 14:34:04

#Intro
One of the common patterns when you design your microservices based apps is an [gateway pattern](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/architect-microservice-container-applications/direct-client-to-microservice-communication-versus-the-api-gateway-pattern) in opposite on client-to-microservice communication. You can write your own gateway applications or use third party solutions for it. One of the widely used solution in .NET world that [Microsoft recommends](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/multi-container-microservice-net-applications/implement-api-gateways-with-ocelot) is [Ocelot](https://ocelot.readthedocs.io/en/latest/introduction/gettingstarted.html).

It is convenient and simple solution for creation API gateways.

#Problem
During of one the recent projects I've faced and interesting problem for one of the microservices that was responsible for files downloading. For files that size was greater than about 250 MB `Ocelot` gateway returned HTTP `Content-Length` Header equals to zero whereas microservice actually returned desired file content. The result of it was an empty downloaded file on client side.
I've found out that it is existing and reported old (dated on Jun 8, 2020) Ocelot issue: [1262](https://github.com/ThreeMammals/Ocelot/issues/1262).
There is also another very similar [issue](https://github.com/ThreeMammals/Ocelot/issues/1382) but solution in comments does not work in my case.

For our application solution of the problem was to override the default `HTTP client` used in Ocelot. I've done it only for one endpoint that is responsible for file downloading just to keep all the implemented Ocelot logic with caching, logging, etc. for rest of configured API.
To do it I've needed to perform 3 steps:

 - To find default implementation of Ocelot `IHttpRequester`. It is a [HttpClientHttpRequester](https://github.com/ThreeMammals/Ocelot/blob/3ef6abd7465fc77632e4b2d5189fbbf47b457867/src/Ocelot/Requester/HttpClientHttpRequester.cs)
 - To implement custom `IHttpRequester` implementation based on [HttpClientHttpRequester](https://github.com/ThreeMammals/Ocelot/blob/3ef6abd7465fc77632e4b2d5189fbbf47b457867/src/Ocelot/Requester/HttpClientHttpRequester.cs) for `contents` endpoint (that is responsible for files downloads):

```cs
public class HttpRequester : IHttpRequester
{
    private readonly IHttpClientCache _cacheHandlers;
    private readonly IOcelotLogger _logger;
    private readonly IDelegatingHandlerHandlerFactory _factory;
    private readonly IExceptionToErrorMapper _mapper;
    private readonly IHttpClientFactory _httpClientFactory;

    public HttpRequester(IOcelotLoggerFactory loggerFactory,
        IHttpClientCache cacheHandlers,
        IDelegatingHandlerHandlerFactory factory,
        IExceptionToErrorMapper mapper, IHttpClientFactory httpClientFactory)
    {
        _logger = loggerFactory.CreateLogger<HttpClientHttpRequester>();
        _cacheHandlers = cacheHandlers;
        _factory = factory;
        _mapper = mapper;
        _httpClientFactory = httpClientFactory;
    }

    public async Task<Response<HttpResponseMessage>> GetResponse(HttpContext httpContext)
    {
        if (httpContext.Request.Path.ToString().Contains("contents"))
        {
            var client = _httpClientFactory.CreateClient("FilesDownloadClient");
            var request = httpContext.Items.DownstreamRequest();
            var response = await client.SendAsync(request.ToHttpRequestMessage(), HttpCompletionOption.ResponseHeadersRead);
            return new OkResponse<HttpResponseMessage>(response);
        }

        var builder = new HttpClientBuilder(_factory, _cacheHandlers, _logger);
        var downstreamRoute = httpContext.Items.DownstreamRoute();
        var downstreamRequest = httpContext.Items.DownstreamRequest();
        var httpClient = builder.Create(downstreamRoute);

        try
        {
            var response = await httpClient.SendAsync(downstreamRequest.ToHttpRequestMessage(), httpContext.RequestAborted);
            return new OkResponse<HttpResponseMessage>(response);
        }
        catch (Exception exception)
        {
            var error = _mapper.Map(exception);
            return new ErrorResponse<HttpResponseMessage>(error);
        }
        finally
        {
            builder.Save();
        }
    }
}
```
 - Register of this custom `IHttpRequester` implementation and `HttpClient` factory in gateway DI container before registering Ocelot:

```cs
builder.Services.AddHttpClient("FilesDownloadClient", client =>
{
    client.Timeout = TimeSpan.FromMinutes(10);
});
builder.Services.TryAddSingleton<IHttpRequester, HttpRequester>();
builder.Services.AddOcelot(configuration);
```
 
Now client can download any files you need through yours Ocelot gateway. 

Important thing in `IHttpRequester` implementation is to have set `HttpCompletionOption.ResponseHeadersRead` (that means the operation should complete as soon as a response is available without buffering and headers are read. The content is not read yet.) during requesting underlying API: 

```cs
var response = await client.SendAsync(request.ToHttpRequestMessage(), HttpCompletionOption.ResponseHeadersRead);
```
You also have to remember that default timeout for `HttpClient` is 100 seconds and for large files gateway can throw an exception. To change timeout you can use:

```cs
builder.Services.AddHttpClient("FilesDownloadClient", client =>
{
    client.Timeout = TimeSpan.FromMinutes(10);
});
```