ASP.NET Core 中的请求超时中间件
作者:Tom Dykstra
应用可以选择性地对请求应用超时限制。 默认情况下,ASP.NET Core 服务器不会执行此操作,因为请求处理时间因方案不同存在很大差异。 例如,WebSocket、静态文件和调用昂贵的 API 需要不同的超时限制。 因此,ASP.NET Core 提供了中间件,可用于配置每个终结点的超时以及全局超时。
达到超时限制时,HttpContext.RequestAborted 中的 CancellationToken 会将 IsCancellationRequested 设置为 true
。 Abort() 不会在请求中自动调用,因此应用程序仍可能生成成功或失败响应。 如果应用不处理异常并生成响应,则默认行为将是返回状态代码 504。
本文介绍如何配置超时中间件。 超时中间件可用于所有类型的 ASP.NET Core 应用:最小 API、带控制器的 Web API、MVC 和 Razor Pages。 示例应用是一个最小 API,但其他应用类型也支持它所演示的每个超时功能。
请求超时位于命名空间 Microsoft.AspNetCore.Http.Timeouts 中。
注意:应用在调试模式下运行时,超时中间件不会触发。 此行为与 Kestrel 超时相同。 若要测试超时,请运行未附加调试器的应用。
将中间件添加到应用
通过调用 AddRequestTimeouts 将请求中间件添加到服务集合。
通过调用 UseRequestTimeouts 将中间件添加到请求处理管道。
注意
- 在显式调用 UseRouting 的应用中,必须在
UseRouting
之后调用UseRequestTimeouts
。
将中间件添加到应用不会自动开始触发超时。 必须显式配置超时限制。
配置一个终结点或页面
对于最小 API 应用,请通过调用 WithRequestTimeout 或应用 [RequestTimeout]
属性将终结点配置为超时,如以下示例所示:
using Microsoft.AspNetCore.Http.Timeouts;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRequestTimeouts();
var app = builder.Build();
app.UseRequestTimeouts();
app.MapGet("/", async (HttpContext context) => {
try
{
await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
}
catch (TaskCanceledException)
{
return Results.Content("Timeout!", "text/plain");
}
return Results.Content("No timeout!", "text/plain");
}).WithRequestTimeout(TimeSpan.FromSeconds(2));
// Returns "Timeout!"
app.MapGet("/attribute",
[RequestTimeout(milliseconds: 2000)] async (HttpContext context) => {
try
{
await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
}
catch (TaskCanceledException)
{
return Results.Content("Timeout!", "text/plain");
}
return Results.Content("No timeout!", "text/plain");
});
// Returns "Timeout!"
app.Run();
对于具有控制器的应用,请将 [RequestTimeout]
属性应用于操作方法或控制器类。 对于 Razor Pages 应用,将属性应用于 Razor 页面类。
配置多个终结点或页面
创建命名策略以指定应用于多个终结点的超时配置。 通过调用 AddPolicy 添加策略:
builder.Services.AddRequestTimeouts(options => {
options.DefaultPolicy =
new RequestTimeoutPolicy { Timeout = TimeSpan.FromMilliseconds(1500) };
options.AddPolicy("MyPolicy", TimeSpan.FromSeconds(2));
});
可以按策略名称为终结点指定超时:
app.MapGet("/namedpolicy", async (HttpContext context) => {
try
{
await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
}
catch (TaskCanceledException)
{
return Results.Content("Timeout!", "text/plain");
}
return Results.Content("No timeout!", "text/plain");
}).WithRequestTimeout("MyPolicy");
// Returns "Timeout!"
属性 [RequestTimeout]
还可用于指定命名策略。
设置全局默认超时策略
为全局默认超时配置指定策略:
builder.Services.AddRequestTimeouts(options => {
options.DefaultPolicy =
new RequestTimeoutPolicy { Timeout = TimeSpan.FromMilliseconds(1500) };
options.AddPolicy("MyPolicy", TimeSpan.FromSeconds(2));
});
默认超时适用于未指定超时的终结点。 以下终结点代码会检查超时,尽管它不会调用扩展方法或应用属性。 由于全局超时配置适用,因此代码会检查超时:
app.MapGet("/", async (HttpContext context) => {
try
{
await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
}
catch
{
return Results.Content("Timeout!", "text/plain");
}
return Results.Content("No timeout!", "text/plain");
});
// Returns "Timeout!" due to default policy.
在策略中指定状态代码
RequestTimeoutPolicy 类具有一个属性,该属性可以在触发超时时自动设置状态代码。
builder.Services.AddRequestTimeouts(options => {
options.DefaultPolicy = new RequestTimeoutPolicy {
Timeout = TimeSpan.FromMilliseconds(1000),
TimeoutStatusCode = 503
};
options.AddPolicy("MyPolicy2", new RequestTimeoutPolicy {
Timeout = TimeSpan.FromMilliseconds(1000),
WriteTimeoutResponse = async (HttpContext context) => {
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync("Timeout from MyPolicy2!");
}
});
});
app.MapGet("/", async (HttpContext context) => {
try
{
await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
}
catch (TaskCanceledException)
{
throw;
}
return Results.Content("No timeout!", "text/plain");
});
// Returns status code 503 due to default policy.
在策略中使用委托
RequestTimeoutPolicy
类具有一个 WriteTimeoutResponse 属性,该属性可用于在触发超时后自定义响应。
builder.Services.AddRequestTimeouts(options => {
options.DefaultPolicy = new RequestTimeoutPolicy {
Timeout = TimeSpan.FromMilliseconds(1000),
TimeoutStatusCode = 503
};
options.AddPolicy("MyPolicy2", new RequestTimeoutPolicy {
Timeout = TimeSpan.FromMilliseconds(1000),
WriteTimeoutResponse = async (HttpContext context) => {
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync("Timeout from MyPolicy2!");
}
});
});
app.MapGet("/usepolicy2", async (HttpContext context) => {
try
{
await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
}
catch (TaskCanceledException)
{
throw;
}
return Results.Content("No timeout!", "text/plain");
}).WithRequestTimeout("MyPolicy2");
// Returns "Timeout from MyPolicy2!" due to WriteTimeoutResponse in MyPolicy2.
禁用超时
要禁用包括默认全局超时在内的所有超时,请使用 [DisableRequestTimeout]
属性或 DisableRequestTimeout 扩展方法:
app.MapGet("/disablebyattr", [DisableRequestTimeout] async (HttpContext context) => {
try
{
await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
}
catch
{
return Results.Content("Timeout!", "text/plain");
}
return Results.Content("No timeout!", "text/plain");
});
// Returns "No timeout!", ignores default timeout.
app.MapGet("/disablebyext", async (HttpContext context) => {
try
{
await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
}
catch
{
return Results.Content("Timeout!", "text/plain");
}
return Results.Content("No timeout!", "text/plain");
}).DisableRequestTimeout();
// Returns "No timeout!", ignores default timeout.
取消超时
要取消已启动的超时,请使用 IHttpRequestTimeoutFeature 上的 DisableTimeout() 方法。 超时在过期后无法取消。
app.MapGet("/canceltimeout", async (HttpContext context) => {
var timeoutFeature = context.Features.Get<IHttpRequestTimeoutFeature>();
timeoutFeature?.DisableTimeout();
try
{
await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
}
catch (TaskCanceledException)
{
return Results.Content("Timeout!", "text/plain");
}
return Results.Content("No timeout!", "text/plain");
}).WithRequestTimeout(TimeSpan.FromSeconds(1));
// Returns "No timeout!" since the default timeout is not triggered.