aspnetcore: Request timeouts middleware

Background and Motivation

A common customer request is to be able to apply timeouts to their requests. AspNetCore servers don’t do this by default since request times vary widely by scenario and we don’t have good ways to predict that. E.g. WebSockets, static files, expensive APIs, etc…

To provide more control we can provide timeouts via middleware that are configured per-endpoint, as well as a global timeout if desired. These timeouts would link to the RequestAborted CancellationToken and be entirely cooperative. E.g. we won’t call Abort when the timeout fires. It’s up to the application logic to consume RequestAborted and decide how to handle the cancellation.

Proposed API


Project/Assembly: Microsoft.AspNetCore.Http

namespace Microsoft.Extensions.DependencyInjection;

+ public static class RequestTimeoutsIServiceCollectionExtensions
+ {
+     public static IServiceCollection AddRequestTimeouts(this IServiceCollection services) { }
+     public static IServiceCollection AddRequestTimeouts(this IServiceCollection services, Action<RequestTimeoutOptions> configure) { }
+     // We need to consider how these policies would integrate with IConfiguration, but that may be substantially different from this API.
+     // public static IServiceCollection AddRequestTimeouts(this IServiceCollection services, IConfigurationSection section) { }
+ }

namespace Microsoft.AspNetCore.Builder;

+ public static class RequestTimeoutsIApplicationBuilderExtensions
+ {
+     public static IApplicationBuilder UseRequestTimeouts(this IApplicationBuilder builder) { }
+ }

+ public static class RequestTimeoutsIEndpointConventionBuilderExtensions
+ {
+     public static IEndpointConventionBuilder WithRequestTimeout(this IEndpointConventionBuilder builder, TimeSpan timeout)  { }
+     public static IEndpointConventionBuilder WithRequestTimeout(this IEndpointConventionBuilder builder, string policyName) { }
+     public static IEndpointConventionBuilder WithRequestTimeout(this IEndpointConventionBuilder builder, RequestTimeoutPolicy policy) { }
+     public static IEndpointConventionBuilder DisableRequestTimeout(this IEndpointConventionBuilder builder) { }
+ }

+ namespace Microsoft.AspNetCore.Http.Timeouts;

+ public class RequestTimeoutOptions
+ {
+     // Applied to any request without a policy set. No value by default.
+     public TimeSpan? DefaultTimeout { get; set; }
+     public RequestTimeoutOptions AddPolicy(string policyName, TimeSpan timeout) { }
+     public RequestTimeoutOptions AddPolicy(string policyName, RequestTimeoutPolicy policy) { }
+     public bool TryGetPolicy(string policyName, out RequestTimeoutPolicy policy) { }
+     public void RemovePolicy(string policyName) { }
+ }

+ public class RequestTimeoutPolicy
+ {
+     public TimeSpan? Timeout { get; }
+     public RequestTimeoutPolicy(TimeSpan? timeout) { }
+ }

+ // Overrides the global timeout, if any.
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
+ public sealed class RequestTimeoutAttribute : Attribute
+ {
+    public TimeSpan? Timeout { get; }
+    public string? PolicyName { get; }
+    public RequestTimeoutAttribute(int seconds) { }
+    public RequestTimeoutAttribute(string policyName) { }
+ }

+ // Disables all timeouts, even the global one.
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
+ public sealed class DisableRequestTimeoutAttribute : Attribute
+ {
+    public DisableRequestTimeoutAttribute() { }
+ }

Usage Examples

services.AddRequestTimeout(options =>
{
    options.DefaultTimeout = TimeSpan.FromSeconds(10);
    options.DefaultTimeoutLocator = context => TimeSpan.Parse(context.Request.Query["delay"]);
    options.AddPolicy("MyTimeoutPolicy", TimeSpan.FromSeconds(1));
    options.AddPolicy("DynamicPolicy", new RequestTimeoutPolicy(context => TimeSpan.Parse(context.Request.Query["timeout"])));
});
...
app.UseRequestTimeouts();
...
app.UseEndpoints(endpoints => {
    endpoints
        .MapGet("/", (context) => { ... })
        .WithRequestTimeout(TimeSpan.FromSeconds(1));
app.UseEndpoints(endpoints => {
    endpoints
        .MapGet("/", (context) => { ... })
        .WithRequestTimeout("MyTimeoutPolicy");
...
    [RequestTimeout(seconds: 1)]
    public ActionResult<TValue> GetHello()
    {
        return "Hello";
    }
    [RequestTimeout("MyTimeoutPolicy")]
    public ActionResult<TValue> GetHello()
    {
        return "Hello";
    }
    [DisableRequestTimeout]
    public ActionResult<TValue> ImVerySlow()
    {
        Thread.Sleep(TimeSpan.FromHours(1);
        return "Hello";
    }

Alternative Designs

Risks

  • We need to ensure this system is flexible enough to cover a wide variety of scenarios, while being easy enough for people to not mis-configure.
  • What about components that don’t use endpoints?
  • What if the middleware is placed in the wrong location?

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 1
  • Comments: 32 (32 by maintainers)

Most upvoted comments

How about also adding DisableRequestTimeout on IEndpointConventionBuilder?

+ public static class RequestTimeoutsIEndpointConventionBuilderExtensions
+ {
+     public static IEndpointConventionBuilder DisableRequestTimeout(this IEndpointConventionBuilder builder) { }
+ }

You mention DefaultTimeoutLocator to handle things like static files. Something that was discussed in Output Caching too, is how we should be able to trigger these middleware when there is no specific endpoint, for specific routing patterns that would be combined with the selected endpoint. For instance be able to do app.AddEndpointMetadata(“/static/css/*”}).WithTimeout(TimeSpan.FromSeconds(5)). Could even be using regexes if required. The metadata would then be added to the resolved endpoint’s metadata. The same way metadata can be applied on endpoint “groups”.

https://github.com/dotnet/aspnetcore/issues/43642