In this article, we’ll explore what the request timeouts middleware in ASP.NET Core does, how we can use it, as well as some advanced features we can leverage.
Let’s dive in!
Timeouts Middleware in ASP.NET Core
Applications using ASP.NET Core don’t apply global timeouts to HTTP requests by default. This is due to the simple reason that, as we all know, different endpoint requests can take drastically different times to return a result depending on various scenarios. Nevertheless, ASP.NET Core does provide us with the option to configure request timeouts via a middleware in .NET 8.
Moreover, using this middleware, we can configure global timeouts as well as specific endpoint timeouts. What’s even better is that we can use timeouts with minimal APIs, controllers, MVC, and even Razor Pages.
Specifically, when a timeout limit kicks in, the middleware updates the HttpContext.RequestAborted
, by setting IsCancellationRequested
to true
. The requests are not automatically aborted; therefore, we can still return a result if the application doesn’t act on the IsCancellationRequested
. The default behavior of ASP.NET Core is to return status code 504
.
Configuring the Timeout Middleware
Before we can configure the middleware, we need to create a simple Web API project. Specifically, our API will deal with Star Wars characters and return them upon our request.
Next, we create a simple Character
class:
public class Character { public required string Name { get; set; } public required int Height { get; set; } public required string BirthYear { get; set; } public required string Gender { get; set; } }
In the class, we add four properties that will hold information about a particular character from the movies.
Following this, we add a service class:
public class StarWarsCharacterService : ICharacterService { public async Task<Character> GetCharacterAsync(CancellationToken cancellationToken) { await Task.Delay(2000, cancellationToken); return new() { Name = "Luke Skywalker", Height = 172, BirthYear = "19BBY", Gender = "Male" }; ; } }
In detail, the StarWarsCharacterService
has one method that uses the Task.Delay()
method to postpone the execution for two seconds and then return the details for Luke Skywalker.
Adding the Request Timeouts Middleware
Now, let’s add the middleware:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddRequestTimeouts(); builder.Services.AddSwaggerGen(); builder.Services.AddTransient<ICharacterService, StarWarsCharacterService>(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseRequestTimeouts(); app.UseAuthorization(); app.MapControllers(); app.Run();
In the implementation of our Program.cs
file, we start by calling the AddRequestTimeouts()
method on our service collection. Subsequently, after this is done, we use the UseRequestTimeouts()
method to add the middleware to the pipeline.
Interestingly, those two lines are all we need to add the request timeouts middleware to our application. Moreover, there are several important things we must note here.
The first one is that timeouts are not configured by default, we must configure them. The second one is that if we are using the routing middleware, we must place our UseRequestTimeouts()
method call after the UseRouting()
method call. Finally, if we want to run and test this locally, we need to detach the debug process otherwise the timeouts won’t apply.
Configuring the Request Timeouts Middleware for Minimal APIs
Additionally, for minimal APIs, we have two options to configure the timeout – using either the attribute or the extension method.
Let’s start with the attribute:
app.MapGet("/GetCharacter", [RequestTimeout(milliseconds: 1000)] async (HttpContext context, ICharacterService characterService) => { return await characterService.GetCharacterAsync(context.RequestAborted); });
We create the GetCharacter
endpoint. It takes in an HttpContext
and a ICharacterService
. Inside the endpoint, we call the GetCharacterAsync()
method of our service and we pass context.RequestAborted
property which is a CancellationToken
. We also decorate the endpoint with the RequestTimeout
attribute and set the timeout to 1 second.
Alternatively, we can use the WithRequestTimeout()
extension method:
app.MapGet("/GetCharacter", async (HttpContext context, ICharacterService characterService) => { return await characterService.GetCharacterAsync(context.RequestAborted); }) .WithRequestTimeout(TimeSpan.FromMilliseconds(1000));
We remove the attribute from our endpoint and add the WithRequestTimeout()
method at the end. We also pass a TimeSpan
representing the duration after which the request should be timed out.
Configuring the Request Timeouts Middleware for Controllers
It’s very easy to use the middleware with regular controllers as well:
[ApiController] [Route("[controller]")] public class StarWarsController : ControllerBase { private readonly ICharacterService _characterService; public StarWarsController(ICharacterService characterService) { _characterService = characterService; } [HttpGet("GetCharacter")] [RequestTimeout(milliseconds: 1000)] public async Task<Character> GetCharacterAsync() => await _characterService.GetCharacterAsync(HttpContext.RequestAborted); }
Here, we create a controller and inject the service. Then, we create the GetCharacter
endpoint which calls the service and again passes the RequestAborted
property. With controller endpoints, the only way to use timeouts is by attributes, so we add the RequestTimeout
attribute to our endpoint.
If we want the same request timeout for all of the endpoints within the controller, we can decorate the controller itself with the attribute.
Advanced Configuration Options
The request timeouts middleware provides us with ample options to fine-tune the conditions under which requests will time out.
Now, let’s take a look at some of them:
Configuring Multiple Endpoints with Named Policies
We can create named policies:
builder.Services.AddRequestTimeouts(options => { options.AddPolicy("OneSecondTimeout", TimeSpan.FromMilliseconds(1000)); });
Inside the AddRequestTimeouts()
method in our Program
class, we use the AddPolicy()
method to create a named policy. We start by passing in the name followed by the delay we want. After this is done we can start applying the policy to the endpoints we desire by either passing the name of the policy to the RequestTimeout
attribute or the WithRequestTimeout()
method.
Applying this policy is easy:
app.MapGet("/GetCharacter", [RequestTimeout("OneSecondTimeout")] async (HttpContext context, ICharacterService characterService) => { return await characterService.GetCharacterAsync(context.RequestAborted); }); app.MapGet("/GetCharacter", async (HttpContext context, ICharacterService characterService) => { return await characterService.GetCharacterAsync(context.RequestAborted); }) .WithRequestTimeout("OneSecondTimeout");
For minimal APIs, we can use the two, already familiar approaches. We can either pass the name of our named policy to the RequestTimeout
attribute or to the WithRequestTimeout()
extension method.
The controller endpoint looks similar to the first example of the minimal API:
[HttpGet("GetCharacter")] [RequestTimeout("OneSecondTimeout")] public async Task<Character> GetCharacterAsync() => await _characterService.GetCharacterAsync(HttpContext.RequestAborted);
We use the RequestTimeout
attribute to apply the named policy to the GetCharacter
endpoint. That’s it!
Setting Global Default Request Timeouts Policy
If needed, we can set up a global timeout:
builder.Services.AddRequestTimeouts(options => { options.DefaultPolicy = new RequestTimeoutPolicy { Timeout = TimeSpan.FromMilliseconds(1500) }; options.AddPolicy("OneSecondTimeout", TimeSpan.FromMilliseconds(1000)); });
Consequently we achieve this by setting the DefaultPolicy
property to a RequestTimeoutPolicy
object we create. The global timeout will apply to all endpoints that don’t have a custom timeout defined.
Let’s see how it works:
[HttpGet("GetCharacter")] [RequestTimeout("OneSecondTimeout")] public async Task<Character> GetCharacterAsync() => await _characterService.GetCharacterAsync(HttpContext.RequestAborted); [HttpGet("GetCharacterWithDefaultTimeout")] public async Task<Character> GetCharacterWithDefaultTimeoutAsync() => await _characterService.GetCharacterAsync(HttpContext.RequestAborted);
Subsequently, within our StarWarsController
, we add the GetCharacterWithDefaultTimeout
endpoint. As we have configured a global timeout policy there is no need to do anything else.
In this example, calls to both endpoints will be timed out. A call to the GetCharacter
endpoint will time out after one second due to the named policy and to the GetCharacterWithDefaultTimeout
endpoint – after one and a half seconds due to the global policy. The same scenario can be applied to minimal APIs as well.
Setting Status Code in Request Timeouts Policy
Furthermore, the middleware also allows us to set a specific default status code:
builder.Services.AddRequestTimeouts(options => { options.DefaultPolicy = new RequestTimeoutPolicy { Timeout = TimeSpan.FromMilliseconds(1500), TimeoutStatusCode = (int)HttpStatusCode.InternalServerError }; options.AddPolicy("OneSecondTimeout", TimeSpan.FromMilliseconds(1000)); });
To set the global status code we set the TimeoutStatusCode
property of the RequestTimeoutPolicy
instance to HttpStatusCode.InternalServerError
. As a result, by doing this, every request that times out will have a status code of 500
(Internal Server Error).
Disabling Request Timeouts for Specific Endpoints
There are often special cases or endpoints that we would like to exclude from our default timeout policies. Thankfully the request timeouts middleware provides a means for disabling the timeout on specific endpoints.
For minimal APIs we can disable the timeouts using the DisableRequestTimeout
attribute:
app.MapGet("/GetCharacter", [DisableRequestTimeout] async (HttpContext context, ICharacterService characterService) => { return await characterService.GetCharacterAsync(context.RequestAborted); })
We can also disable timeouts via the DisableRequestTimeout()
extension method:
app.MapGet("/GetCharacter", async (HttpContext context, ICharacterService characterService) => { return await characterService.GetCharacterAsync(context.RequestAborted); }) .DisableRequestTimeout();
For controllers, we must use the DisableRequestTimeout
attribute to override the default timeout policy:
[HttpGet("GetCharacter")] [DisableRequestTimeout] public async Task<Character> GetCharacterAsync() => await _characterService.GetCharacterAsync(HttpContext.RequestAborted);
Conclusion
The request timeouts middleware is a great addition to .NET 8, which helps in ensuring applications remain robust and responsive in the face of varying processing times. Its configuration is seamless, be it for Minimal APIs or controllers. Advanced options, from named policies to global defaults, empower us to have full control over request timeouts in our application by tailoring a combination of global and custom policies.