ASP.NET Core の要求タイムアウト ミドルウェア

著者: Tom Dykstra

要求のタイムアウト制限は、アプリから選択的に適用することができます。 要求の処理時間はシナリオによって大きく異なるため、ASP.NET Core サーバーからは既定では適用されません。 たとえば、WebSocket、静的ファイル、長時間の処理を伴う API 呼び出しには、それぞれ異なるタイムアウト制限が必要になります。 そのため ASP.NET Core には、エンドポイントごとのタイムアウトとグローバルなタイムアウトを構成するミドルウェアが用意されています。

タイムアウト制限に達すると、HttpContext.RequestAbortedCancellationToken によって IsCancellationRequestedtrue に設定されます。 要求に対して Abort() が自動的に呼び出されることはないため、アプリケーションは、成功と失敗のどちらの応答を生成することもできます。 アプリが例外を処理せず応答も生成しない場合、既定では、状態コード 504 を返す動作が行われます。

この記事では、タイムアウト ミドルウェアを構成する方法について説明します。 タイムアウト ミドルウェアは、すべての種類の ASP.NET Core アプリ (Minimal API、コントローラーを使用した Web API、MVC、Razor Pages) で使用できます。 サンプル アプリは Minimal API ですが、例で使用しているすべてのタイムアウト機能は他の種類のアプリでもサポートされています。

要求タイムアウトは Microsoft.AspNetCore.Http.Timeouts 名前空間に属します。

注: アプリがデバッグ モードで実行されている場合、タイムアウト ミドルウェアはトリガーされません。 この動作は、Kestrel タイムアウトの場合と同じです。 タイムアウトをテストするには、デバッガーをアタッチせずにアプリを実行します。

ミドルウェアをアプリに追加する

サービス コレクションに要求タイムアウト ミドルウェアを追加するには、AddRequestTimeouts を呼び出します。

要求処理パイプラインにミドルウェアを追加するには、UseRequestTimeouts を呼び出します。

Note

  • 明示的に UseRouting を呼び出すアプリでは、UseRouting の後に UseRequestTimeouts を呼び出すことが必須です。

ミドルウェアをアプリに追加しても、タイムアウトのトリガー動作は自動的には開始されません。 タイムアウト制限を明示的に構成する必要があります。

1 つのエンドポイントまたはページを構成する

Minimal 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.

タイムアウトを取り消す

開始済みのタイムアウトを取り消すには、IHttpRequestTimeoutFeatureDisableTimeout() メソッドを使用します。 期限が切れた後のタイムアウトを取り消すことはできません。

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.

関連項目