asp.net 8 中的 RequestTimeoutsMiddleware
http:// 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");