相关文章推荐
要出家的火车  ·  Django+rest_framework ...·  2 周前    · 
想出国的蘑菇  ·  flutter 下载文件 - ...·  1 周前    · 
踢足球的草稿本  ·  Internet Explorer ...·  2 月前    · 
自信的酱肘子  ·  Accessing blob ...·  1 年前    · 
刚毅的韭菜  ·  python ...·  1 年前    · 
asp.net 8 中的 RequestTimeoutsMiddleware

asp.net 8 中的 RequestTimeoutsMiddleware

asp.net 8 中的 RequestTimeoutsMiddleware

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) =>
        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) =>
        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) =>
        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) =>
        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");