Middleware de tempos limite de solicitação no ASP.NET Core

Por Tom Dykstra

Os aplicativos podem aplicar limites de tempo limite seletivamente às solicitações. O servidores do ASP.NET Core não fazem isso por padrão, pois os tempos de processamento de solicitação variam amplamente por cenário. Por exemplo, WebSockets, arquivos estáticos e APIs caras de chamada exigiriam um limite de tempo limite diferente. Portanto, o ASP.NET Core fornece middleware que configura tempos limite por ponto de extremidade, bem como um tempo limite global.

Quando um limite de tempo limite é atingido, um CancellationToken em HttpContext.RequestAborted tem IsCancellationRequested definido como true. Abort() não é chamado automaticamente na solicitação, portanto, o aplicativo ainda pode produzir uma resposta de êxito ou falha. O comportamento padrão se o aplicativo não tratar a exceção e produzir uma resposta é retornar o código de status 504.

Esse artigo explica como configurar o middleware de tempo limite. O middleware de tempo limite pode ser usado em todos os tipos de aplicativos ASP.NET Core: API mínima, API Web com controladores, MVC e Páginas do Razor. O aplicativo de exemplo é uma API Mínima, mas todos os recursos de cache ilustrados por ele também têm suporte nos outros tipos de aplicativo.

Os tempos limite de solicitação estão no namespace Microsoft.AspNetCore.Http.Timeouts.

Observação: quando um aplicativo está em execução no modo de depuração, o middleware de tempo limite não é disparado. Esse comportamento semelhante aos tempos limite Kestrel. Para testar tempos limite, execute o aplicativo sem o depurador anexado.

Adicionar o middleware ao aplicativo

Adicione o middleware de tempo limite de solicitação à coleção de serviços chamando AddRequestTimeouts.

Adicione o middleware ao pipeline de processamento de solicitação chamando UseRequestTimeouts.

Observação

  • Em aplicativos que chamam UseRoutingexplicitamente , UseRequestTimeouts deve ser chamado após UseRouting.

Adicionar o middleware ao aplicativo não inicia automaticamente o disparo de tempos limite. Os limites de tempo limite precisam ser configurados explicitamente.

Configurar um ponto de extremidade ou página

Para aplicativos de API mínima, configure um ponto de extremidade para o tempo limite chamando WithRequestTimeout ou aplicando o atributo [RequestTimeout], conforme mostrado nos exemplos a seguir:

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

Para aplicativos com controladores, aplique o atributo [RequestTimeout] ao método de ação. Para aplicativos de Páginas do Razor, aplique o atributo à classe de página Razor.

Configurar vários pontos de extremidade ou páginas

Crie políticas nomeadas para especificar a configuração de tempo limite que se aplica a vários pontos de extremidade. Adicione uma política chamando AddPolicy:

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

Um tempo limite pode ser especificado para um ponto de extremidade pelo nome da política:

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

O atributo [RequestTimeout] também pode ser usado para especificar uma política nomeada.

Definir a política de tempo limite padrão global

Especifique uma política para a configuração de tempo limite padrão global:

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

O tempo limite padrão se aplica a pontos de extremidade que não têm um tempo limite especificado. O código de ponto de extremidade a seguir verifica se há um tempo limite, embora não chame o método de extensão ou aplique o atributo . A configuração de tempo limite global se aplica, portanto, o código verifica se há um tempo limite:

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.

Especificar o código de status em uma política

A classe RequestTimeoutPolicy tem uma propriedade que pode definir automaticamente o código status quando um tempo limite é disparado.

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.

Usar um delegado em uma política

A classe RequestTimeoutPolicy tem uma propriedade WriteTimeoutResponse que pode ser usada para personalizar a resposta quando um tempo limite é disparado.

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.

Como desabilitar tempos limite

Para desabilitar todos os tempos limite, incluindo o tempo limite global padrão, use o atributo [DisableRequestTimeout] ou o método de extensão 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.

Como cancelar um tempo limite

Para cancelar um tempo limite que já foi iniciado, use o método DisableTimeout() em IHttpRequestTimeoutFeature. Os tempos limite não podem ser cancelados depois de expirarem.

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.

Confira também