Time-out requests in ASP.NET Core with cancellation tokens
Tags: csharp, aspnetcore, dotnet, webdev
Published: 2022-12-26 09:20:42

## Introduction

When ASP.NET Core is running in an AWS Lambda and receiving requests through an AWS API Gateway, the application is not notified of an API Gateway time-out and keeps processing the request, completing it eventually. This will leave metrics and logs of a successful request when the client received a time-out error.

![](https://blog.genezini.com/p/time-out-requests-in-asp.net-core-with-cancellation-tokens/images/problem.gif)

In this post, I'll show how to solve this problem with cancellation tokens and time-outs.

## The solution

The idea is to time-out the request in the ASP.NET application before the API Gateway time-out. This way we can collect logs and metrics of the requests that timed out and have a realistic view of the errors the users are seeing. 

![](https://blog.genezini.com/p/time-out-requests-in-asp.net-core-with-cancellation-tokens/images/solution.gif)

In [this previous post](https://blog.genezini.com/p/canceling-abandoned-requests-in-asp.net-core/), I've explained how to cancel requests aborted by the HTTP client using the `CancellationToken` in the controllers methods.

The default ASP.NET's model binding for the `CancellationToken` injects the `RequestAborted` property of the `HttpContext` object in the controller methods. The `RequestAborted` property holds a `CancellationToken` that is triggered when the client abandons the request.

Let's create a middleware to apply the timeout to ASP.NET Core's Cancellation Token.

First, we'll create a new `CancellationTokenSource` and set a time-out to it.

Then, we'll use the `CancellationToken.CreateLinkedTokenSource` method to link the `HttpContext.RequestAborted` CancellationToken to the new CancellationToken we created with a time-out.

Lastly, we override the `HttpContext.RequestAborted` Cancellation Token with the token returned by the CreateLinkedTokenSource.

> ⚠️ Remember to always dispose of the `CancellationTokenSource` objects to avoid memory leaks. Use the `using` keyword or dispose of them in the `HttpContext.Response.OnCompleted` delegate.

```csharp
public class TimeoutCancellationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly TimeSpan _timeout;

    public TimeoutCancellationMiddleware(RequestDelegate next, TimeoutCancellationMiddlewareOptions options)
    {
        _next = next;
        _timeout = options.Timeout;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        //Create a new CancellationTokenSource and set a time-out
        using var timeoutCancellationTokenSource = new CancellationTokenSource();
        timeoutCancellationTokenSource.CancelAfter(_timeout);

        //Create a new CancellationTokenSource linking the timeoutCancellationToken and ASP.NET's RequestAborted CancellationToken
        using var combinedCancellationTokenSource =
            CancellationTokenSource
                .CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, context.RequestAborted);

        //Override the RequestAborted CancellationToken with our combined CancellationToken
        context.RequestAborted = combinedCancellationTokenSource.Token;

        await _next(context);
    }
}

public class TimeoutCancellationMiddlewareOptions
{
    public TimeSpan Timeout { get; set; }

    public TimeoutCancellationMiddlewareOptions(TimeSpan timeout)
    {
        Timeout = timeout;
    }
}
```

We can also create an extension method to make it easier to configure the middleware:

```csharp
public static class TimeoutCancellationMiddlewareExtensions
{
    public static IApplicationBuilder UseTimeoutCancellationToken(
        this IApplicationBuilder builder, TimeSpan timeout)
    {
        return builder.UseMiddleware<TimeoutCancellationMiddleware>(
            new TimeoutCancellationMiddlewareOptions(timeout));
    }
}
```

Then, add it to the middleware pipeline in our application:

```csharp
public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        builder.Services.AddControllers();

        var app = builder.Build();

        ...

        //Configure a request time-out of 10 seconds
        app.UseTimeoutCancellationToken(TimeSpan.FromSeconds(10));

        ...

        app.Run();
    }
}
```

Now, we have to use the `CancellationToken` injected in the controllers' methods and pass it down to all async methods.

DogImageController:

```csharp
[ApiController]
[Route("[controller]")]
public class DogImageController : ControllerBase
{
    private readonly IDogApi _dogApi;
    private readonly ILogger<DogImageController> _logger;

    public DogImageController(IDogApi dogApi, ILogger<DogImageController> logger)
    {
        _dogApi = dogApi;
        _logger = logger;
    }

    [HttpGet]
    public async Task<ActionResult<string>> GetAsync(CancellationToken cancellationToken)
    {
        try
        {
            var dog = await _dogApi.GetRandomDog(cancellationToken);

            return dog.message;
        }
        catch(TaskCanceledException)
        {
            _logger.LogError(ex, ex.Message);

            return StatusCode(StatusCodes.Status500InternalServerError, "Request timed out or canceled");
        }
    }
}
```

DogImageUseCase:

```csharp
public class DogImageUseCase: IDogImageUseCase
{
    private readonly IDogApi _dogApi;

    public DogImageUseCase(IDogApi dogApi)
    {
        _dogApi = dogApi;
    }

    public async Task<string> GetRandomDogImage(CancellationToken cancellationToken)
    {
        var dog = await _dogApi.GetRandomDog(cancellationToken);

        return dog.message;
    }
}
```

## Testing the solution

To test if it works, let's force a delay in the `DogImageUseCase` class:

```csharp
public class DogImageUseCase: IDogImageUseCase
{
    private readonly IDogApi _dogApi;

    public DogImageUseCase(IDogApi dogApi)
    {
        _dogApi = dogApi;
    }

    public async Task<string> GetRandomDogImage(CancellationToken cancellationToken)
    {
        var dog = await _dogApi.GetRandomDog(cancellationToken);

        await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken);

        return dog.message;
    }
}
```

![Request timing out](https://blog.genezini.com/p/time-out-requests-in-asp.net-core-with-cancellation-tokens/images/timeout.gif)

## Creating an integration test for the time-out scenario

In [this previous post](https://blog.genezini.com/p/integration-tests-without-api-dependencies-with-asp.net-core-and-wiremock.net/), I explained how to use `WireMock.Net` to mock API dependencies in integration tests.

Now, we'll use `WireMock.Net` to create a mock that responds with a delay. This way, we can validate that our application is stopping the processing of the request when the time-out occurs.

First, we override the `HttpClientTimeoutSeconds` configuration value to a time-out bigger than our request time-out. This configuration is used to set the `Timeout` property of the `HttpClient`. If the HttpClient time-out is smaller than our request time-out, it will abort the request to the dependencies before the application times out.

Then, we use WireMock's `AddGlobalProcessingDelay` to insert a delay in the API mock and force our application to time-out before the response.

Lastly, we assert that our application returned the status code `500` with the message `Request timed out or canceled`.

```csharp
public class DogImageTests: IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public DogImageTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    ...

    [Fact]
    public async Task Timeout_Returns_500WithMessage()
    {
        //Arrange
        var wireMockSvr = WireMockServer.Start();

        var factory = _factory
            .WithWebHostBuilder(builder =>
            {
                builder.UseSetting("DogApiUrl", wireMockSvr.Url);

                //Set the HttpClient timeout to 30, so it doesn't trigger before our request timeout that is 10 seconds
                builder.UseSetting("HttpClientTimeoutSeconds", "30");

                //Set the Request time-out to 5 seconds for the test to run faster
                builder.UseSetting("RequestTimeoutSeconds", "5");  
            });

        var httpClient = factory.CreateClient();

        Fixture fixture = new Fixture();

        var responseObj = fixture.Create<Dog>();
        var responseObjJson = JsonSerializer.Serialize(responseObj);

        wireMockSvr
            .Given(Request.Create()
                .WithPath("/breeds/image/random")
                .UsingGet())
            .RespondWith(Response.Create()
                .WithBody(responseObjJson)
                .WithHeader("Content-Type", "application/json")
                .WithStatusCode(HttpStatusCode.OK));

        //Add a delay to the response to cause a request timeout in the /DogImage endpoint
        wireMockSvr.AddGlobalProcessingDelay(TimeSpan.FromSeconds(15));

        //Act
        var apiHttpResponse = await httpClient.GetAsync("/DogImage");

        //Assert
        apiHttpResponse.StatusCode.Should().Be(HttpStatusCode.InternalServerError);

        var responseMessage = await apiHttpResponse.Content.ReadAsStringAsync();

        responseMessage.Should().BeEquivalentTo("Request timed out or canceled");

        wireMockSvr.Stop();
    }
}
```

## Complete code

Check it on the [GitHub Repository](https://github.com/dgenezini/RequestTimeoutSample).

## Liked this post?

I post extra content in my personal blog. [Click here](https://blog.genezini.com/) to see.

## Follow me

- [On Feedly](https://feedly.com/i/subscription/feed/https://blog.genezini.com/index.xml)
- [RSS](https://blog.genezini.com/index.xml)
- [Linkedin](https://www.linkedin.com/in/danielgenezini/)
- [Twitter](https://twitter.com/GeneziniDaniel)

## References and links

- [Integration tests without API dependencies with ASP.NET Core and WireMock.Net](https://blog.genezini.com/p/integration-tests-without-api-dependencies-with-asp.net-core-and-wiremock.net/)
- [Canceling abandoned requests in ASP.NET Core](https://blog.genezini.com/p/canceling-abandoned-requests-in-asp.net-core/)
- [Cancellation in Managed Threads](https://learn.microsoft.com/en-us/dotnet/standard/threading/cancellation-in-managed-threads?redirectedfrom=MSDN)