Oprogramowanie pośredniczące przekroczenia limitu czasu żądania w programie ASP.NET Core

Autor: Tom Dykstra

Aplikacje mogą selektywnie stosować limity czasu do żądań. serwery ASP.NET Core nie robią tego domyślnie, ponieważ czasy przetwarzania żądań różnią się znacznie w zależności od scenariusza. Na przykład obiekty WebSocket, pliki statyczne i wywoływanie kosztownych interfejsów API wymagają innego limitu czasu. Dlatego ASP.NET Core udostępnia oprogramowanie pośredniczące, które konfiguruje limity czasu dla punktu końcowego, a także globalny limit czasu.

Po osiągnięciu CancellationToken limitu czasu dla elementu in HttpContext.RequestAborted ustawiono IsCancellationRequested wartość true. Abort() nie jest automatycznie wywoływana w żądaniu, więc aplikacja może nadal generować odpowiedź na powodzenie lub niepowodzenie. Domyślne zachowanie, jeśli aplikacja nie obsługuje wyjątku i generuje odpowiedź, jest zwrócenie kodu stanu 504.

W tym artykule wyjaśniono, jak skonfigurować oprogramowanie pośredniczące limitu czasu. Oprogramowanie pośredniczące limitu czasu może być używane we wszystkich typach aplikacji ASP.NET Core: minimalny interfejs API, internetowy interfejs API z kontrolerami, MVC i Razor stronami. Przykładowa aplikacja jest minimalnym interfejsem API, ale każda funkcja przekroczenia limitu czasu jest również obsługiwana w innych typach aplikacji.

Limity czasu żądania znajdują się w Microsoft.AspNetCore.Http.Timeouts przestrzeni nazw.

Uwaga: gdy aplikacja jest uruchomiona w trybie debugowania, oprogramowanie pośredniczące limitu czasu nie jest wyzwalane. To zachowanie jest takie samo jak w przypadku Kestrel przekroczenia limitu czasu. Aby przetestować limity czasu, uruchom aplikację bez dołączonego debugera.

Dodawanie oprogramowania pośredniczącego do aplikacji

Dodaj oprogramowanie pośredniczące limitu czasu żądania do kolekcji usług, wywołując metodę AddRequestTimeouts.

Dodaj oprogramowanie pośredniczące do potoku przetwarzania żądań, wywołując metodę UseRequestTimeouts.

Uwaga

  • W aplikacjach, które jawnie wywołają metodę UseRouting, UseRequestTimeouts muszą być wywoływane po UseRouting.

Dodanie oprogramowania pośredniczącego do aplikacji nie powoduje automatycznego uruchamiania wyzwalania limitów czasu. Limity limitów czasu muszą być jawnie skonfigurowane.

Konfigurowanie jednego punktu końcowego lub strony

W przypadku minimalnych aplikacji interfejsu API skonfiguruj punkt końcowy do przekroczenia limitu czasu przez wywołanie WithRequestTimeoutmetody lub przez zastosowanie atrybutu [RequestTimeout] , jak pokazano w poniższym przykładzie:

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();

W przypadku aplikacji z kontrolerami zastosuj [RequestTimeout] atrybut do metody akcji lub klasy kontrolera. W przypadku Razor aplikacji Pages zastosuj atrybut do Razor klasy strony.

Konfigurowanie wielu punktów końcowych lub stron

Utwórz nazwane zasady , aby określić konfigurację limitu czasu, która ma zastosowanie do wielu punktów końcowych. Dodaj zasady, wywołując polecenie AddPolicy:

builder.Services.AddRequestTimeouts(options => {
    options.DefaultPolicy =
        new RequestTimeoutPolicy { Timeout = TimeSpan.FromMilliseconds(1500) };
    options.AddPolicy("MyPolicy", TimeSpan.FromSeconds(2));
});

Limit czasu można określić dla punktu końcowego według nazwy zasad:

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!"

Atrybut [RequestTimeout] można również użyć do określenia nazwanych zasad.

Ustawianie globalnych domyślnych zasad limitu czasu

Określ zasady dla konfiguracji globalnego domyślnego limitu czasu:

builder.Services.AddRequestTimeouts(options => {
    options.DefaultPolicy =
        new RequestTimeoutPolicy { Timeout = TimeSpan.FromMilliseconds(1500) };
    options.AddPolicy("MyPolicy", TimeSpan.FromSeconds(2));
});

Domyślny limit czasu dotyczy punktów końcowych, które nie mają określonego limitu czasu. Poniższy kod punktu końcowego sprawdza limit czasu, chociaż nie wywołuje metody rozszerzenia ani nie stosuje atrybutu. Stosowana jest globalna konfiguracja limitu czasu, więc kod sprawdza limit czasu:

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.

Określanie kodu stanu w zasadach

Klasa RequestTimeoutPolicy ma właściwość, która może automatycznie ustawiać kod stanu po wyzwoleniu limitu czasu.

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.

Używanie delegata w zasadach

Klasa RequestTimeoutPolicy ma WriteTimeoutResponse właściwość, która może służyć do dostosowywania odpowiedzi po wyzwoleniu limitu czasu.

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.

Wyłączanie limitów czasu

Aby wyłączyć wszystkie limity czasu, w tym domyślny limit czasu globalnego, użyj atrybutu [DisableRequestTimeout]DisableRequestTimeout lub metody rozszerzenia:

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.

Anulowanie limitu czasu

Aby anulować limit czasu, który został już uruchomiony, użyj DisableTimeout() metody w pliku IHttpRequestTimeoutFeature. Nie można anulować limitów czasu po wygaśnięciu.

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.

Zobacz też