ASP.NET Core 中的请求超时中间件

汤姆·迪克斯特拉

应用可以选择性地对请求应用超时限制。 ASP.NET 核心服务器默认不执行此作,因为请求处理时间因方案而异。 例如,WebSocket、静态文件和调用昂贵的 API 都需要不同的超时限制。 因此,ASP.NET Core 提供中间件,用于配置每个终结点的超时以及全局超时。

达到超时限制时,已CancellationTokenHttpContext.RequestAbortedIsCancellationRequested设置为 。true Abort() 不会在请求上自动调用,因此应用程序仍可能会生成成功或失败响应。 如果应用未处理异常并生成响应,则默认行为是返回状态代码 504。

本文介绍如何配置超时中间件。 超时中间件可用于所有类型的 ASP.NET 核心应用:最小 API、具有控制器、MVC 和 Pages 的 Razor Web API。 示例应用是最小 API,但它演示的每个超时功能在其他应用类型中也受支持。

请求超时位于命名空间中 Microsoft.AspNetCore.Http.Timeouts

注意: 当应用在调试模式下运行时,超时中间件不会触发。 此行为与 Kestrel 超时行为相同。 若要测试超时,请运行未附加调试器的应用。

将中间件添加到应用

通过调用 AddRequestTimeouts将请求超时中间件添加到服务集合。

通过调用 UseRequestTimeouts将中间件添加到请求处理管道。

注释

  • 在显式调用 UseRouting的应用中, UseRequestTimeouts 必须在之后 UseRouting调用。

将中间件添加到应用不会自动启动触发超时。 必须显式配置超时限制。

配置一个终结点或页面

对于最小的 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.

取消超时

若要取消已启动的超时,请使用 DisableTimeout() 方法打开 IHttpRequestTimeoutFeature。 超时在过期后无法取消。

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.

另请参阅