ASP.NET Core 中的要求逾時中介軟體

作者:Tom Dykstra

應用程式可以選擇性地將逾時限制套用至要求。 ASP.NET 核心伺服器預設不會這麼做,因為要求處理時間會因情節而異。 例如,WebSockets、靜態檔案和呼叫昂貴的 API 都需要不同的逾時限制。 因此,ASP.NET Core 提供中介軟體,以設定每個端點的逾時以及全域逾時。

達到逾時限制時,HttpContext.RequestAborted 中的 CancellationToken 會將 IsCancellationRequested 設定為 trueAbort() 不會在要求上自動呼叫,因此應用程式仍可能會產生成功或失敗的回應。 如果應用程式未處理例外狀況並產生回應,則預設行為是傳回狀態碼 504。

本文說明如何設定逾時中介軟體。 逾時中介軟體可用於所有類型的 ASP.NET Core 應用程式:最小 API、具有控制器的 Web API、MVC 和 Razor 頁面。 範例應用程式是「最小 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 頁面應用程式,將屬性套用至 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.

另請參閱