asp.net 8 中的 RequestTimeoutsMiddleware

目录

Intro

Samples

Global timeout

Specific endpoint timeout

Disable timeout

Look Inside

More

References


Intro

.NET 8 中引入了一个 RequestTimeoutsMiddleware 我们可以借助它来更方便地实现 request 超时的处理

下面我们来看些示例看下如何使用

Samples

Global timeout

最简单的一个示例如下:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRequestTimeouts(options =>
{
    options.DefaultPolicy = new RequestTimeoutPolicy()
    {
        Timeout = TimeSpan.FromSeconds(1)
    };
});
var app = builder.Build();
app.UseRequestTimeouts();
app.Map("/", () => "Hello world");
await app.RunAsync();

这里配置了默认的 timeout policy 为 1s 超时,但是上述我们没有配置 timeout 的 endpoint,我们来加一个测试的 endpoint

app.MapGet("/timeout", async (CancellationToken cancellationToken) =>
{
    await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
    return Results.Content("No timeout!", "text/plain");
});

这里我们加了一个 /timeout 的 endpoint, 在处理逻辑里有 5s 的等待,这样我们就会出现超时的情况,我们来测试一下,这里使用 dotnet-httpie 来测试 API 请求

图片

默认的 response status code 是 504,我们也可以配置 response 的 statusCode 和 timeout 的 response

builder.Services.AddRequestTimeouts(options =>
{
    options.DefaultPolicy = new RequestTimeoutPolicy()
    {
        Timeout = TimeSpan.FromSeconds(1),
        // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408
        TimeoutStatusCode = 408,
        WriteTimeoutResponse = context =>
        {
            return context.Response.WriteAsync("timeout");
        }
    };

这里我们配置了 TimeoutStatusCode 和 Timeout 之后的 response,输出自定义的内容

图片

可以看到此时输出的结果,response statusCode 已经变成了 408 并且也有了一个 response body

Specific endpoint timeout

对于某一个 endpoint 我们也可以单独设置 timeout,单独设置的 timeout 的优先级会高于 global 的 timeout,我们可以 by policy 的配置类似的超时,

比如我们添加一个 2s 超时的 policy

builder.Services.AddRequestTimeouts(options =>
{
    options.DefaultPolicy = new RequestTimeoutPolicy()
    {
        Timeout = TimeSpan.FromSeconds(1),
        WriteTimeoutResponse = context =>
        {
            return context.Response.WriteAsync("timeout");
        }
    };
    options.AddPolicy("timeout-2s", TimeSpan.FromSeconds(2));
});

然后再添加一个 endpoint 引用新加的 policy

app.MapGet("/timeout-policy", async (CancellationToken cancellationToken) =>
    {
        await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
        return Results.Content("No timeout!", "text/plain");
    }).WithRequestTimeout("timeout-2s");

再来测试一下我们的 API

图片

除了引用定义好的 policy 之外,我们还可以直接定义 timeout 时间,response status 以及自定义 response 输出

app.MapGet("/special-timeout-policy", async (CancellationToken cancellationToken) =>
        {
            await Task.Delay(1500, cancellationToken);
            return "Timeout";
        }).WithRequestTimeout(new RequestTimeoutPolicy()
        {
            Timeout = TimeSpan.FromSeconds(1),
            TimeoutStatusCode = 418,
            WriteTimeoutResponse = context => context.Response.WriteAsync("Oh it's timeout")
        });

API 访问输出结果如下:

图片

除了 WithRequestTimeout 方法来设置,我们也可以通过 attribute 来设置

app.MapGet("/timeout-attribute-policy", [RequestTimeout("timeout-2s")]async (CancellationToken cancellationToken) =>
{
    try
    {
        await Task.Delay(800, cancellationToken);
    } 
    catch (TaskCanceledException)
    {
        return Results.Content("Timeout!", "text/plain");
    }
    return Results.Content("No timeout!", "text/plain");
});
app.MapGet("/timeout-attribute", [RequestTimeout(500)]async (CancellationToken cancellationToken) =>
{
    try
    {
        await Task.Delay(800, cancellationToken);
    } 
    catch (TaskCanceledException)
    {
        return Results.Content("Timeout!", "text/plain");
    }
    return Results.Content("No timeout!", "text/plain");
});

这两个示例,分别是使用 policy 和直接定义超时时间,我们也来测试一下

图片

这两个 endpoint response statusCode 是 200 因为我们在处理逻辑里自己处理了 timeout 逻辑

Disable timeout

我们也可以针对某个 endpoint 禁用 timeout,如果不配置 DefaultPolicy 则默认没有 timeout

通过 DisableRequestTimeout 方法来禁用 timeout

app.MapGet("/no-timeout", async (CancellationToken cancellationToken) =>
{
    try
    {
        await Task.Delay(5000, cancellationToken);
    } 
    catch (TaskCanceledException)
    {
        return Results.Content("Timeout!", "text/plain");
    }
    return Results.Content("No timeout!", "text/plain");
}).DisableRequestTimeout();

输出结果如下:

图片

我们也可以使用 attribute 的方式来禁用 timeout

app.MapGet("/no-timeout-attribute", [DisableRequestTimeout]async (CancellationToken cancellationToken) =>
{
    try
    {
        await Task.Delay(5000, cancellationToken);
    } 
    catch (TaskCanceledException)
    {
        return Results.Content("Timeout!", "text/plain");
    }
    return Results.Content("No timeout!", "text/plain");
});

输出结果和上面的方式一样

除此之外还有一个方式,可以通过 IHttpRequestTimeoutFeature.DisableTimeout() 来禁用/取消超时(只有超时之前有效)

app.MapGet("/no-timeout-feature", async (HttpContext context) =>
{
    var timeoutFeature = context.Features.Get<IHttpRequestTimeoutFeature>();
    timeoutFeature?.DisableTimeout();
    await Task.Delay(3000, context.RequestAborted);
    return "No timeout";
});

图片

Look Inside

我们来看下 RequestTimeoutsMiddleware  的部分实现

图片

首先判断了是不是正在使用 debugger 进行调试,如果是则跳过,则不会触发 RequestTimeouts

然后再判断 endpoint 是否有定义 attribute 或者 policy, 如果都没有并且没有定义 DefaultPolicy 则无需处理 request timeout 可以直接走下一个 middleware

之后再看是否定义了要 disable request timeout,如果有要禁用也应该直接走下一个 middleware,无需处理 timeout

最后再看下最终的 timeout 时间,如果没找到对应的 policy 会报错,最终的 timeout 时间如果是 null 或者 Timeout.Infinite 则无需处理,直接走下一个 middleware,由此我们发现了另外一种 disable timeout 的方法

app.MapGet("/no-timeout-infinite", async (CancellationToken cancellationToken) =>
{
    await Task.Delay(1500, cancellationToken);
    return "No Timeout";
}).WithRequestTimeout(Timeout.InfiniteTimeSpan);

输出结果如下:

图片

如果有 timeout 时间,则创建了一个 linked cancellationToken,并且替换原来的 HttpContext.RequestAborted 和设置 IHttpRequestTimeoutFeature,在发生 OperationCanceledException 并且只有在 response 还没开始并且是 timeout 发生的时候才会根据 policy 设置 response ,并在最终会换成原来的 HttpContext.RequestAborted 对应的 CancellationToken,并清空 IHttpRequestTimeoutFeature

图片

SetTimeout

More

最后要提一点,前面的示例大家会发现都使用了一个 CancellationToken 参数,这个等同于 HttpContext.RequestAborted, 我们需要在处理逻辑使用这个 CancellationToken 如果我们的方法是不会用到就不会触发这个 timeout,示例如下:

app.MapGet("/no-timeout-sync",  () =>
{
    Thread.Sleep(5000);
    return Results.Content("No timeout!", "text/plain");
});
app.MapGet("/no-timeout-async",  async () =>
{
    await Task.Delay(5000);
    return Results.Content("No timeout!", "text/plain");
});

这两个方法是没有用到 HttpContext.RequestAborted 这个 CancellationToken 的,输出结果如下:

图片

从输出结果可以看出来,这两个方法均没有发生 timeout,所以如果需要使用 request timeout 功能需要在自己的逻辑实际处理 HttpContext.RequestAborted 才可以生效

attribute 的方式不仅适用于 Minimal API 同样适用于 asp.net controller 

References

  • https://github.com/dotnet/aspnetcore/blob/410efd482f494d1ab05ce25b932b5788699c2308/src/Http/Http/src/Timeouts/RequestTimeoutsMiddleware.cs

  • https://github.com/dotnet/aspnetcore/blob/410efd482f494d1ab05ce25b932b5788699c2308/src/Http/Http/src/Timeouts/HttpRequestTimeoutFeature.cs

  • https://github.com/dotnet/aspnetcore/blob/9355d427269a8c42911131a5fe2b8cd76a63a2c0/src/Http/Http/src/Timeouts/CancellationTokenLinker.cs

  • https://github.com/dotnet/aspnetcore/blob/410efd482f494d1ab05ce25b932b5788699c2308/src/Http/Http/test/Timeouts/RequestTimeoutsMiddlewareTests.cs

  • https://learn.microsoft.com/en-us/aspnet/core/performance/timeouts?view=aspnetcore-8.0

  • https://github.com/WeihanLi/SamplesInPractice/blob/master/net8sample/AspNetCore8Sample/RequestTimeoutsSample.cs

  • https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408

  • https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/418

  • https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504

引入地址 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值