Skip to content

Request timeouts middleware #45732

Closed
@Tratcher

Description

@Tratcher

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?

Metadata

Metadata

Labels

api-approvedAPI was approved in API review, it can be implementedarea-networkingIncludes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions