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. ASP.NET Os servidores 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 chamadas a APIs custosas exigiriam, cada um, um limite de tempo diferente. Assim, o ASP.NET Core fornece um middleware que configura limites de tempo por endpoint, bem como um limite de tempo 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 manipular a exceção e produzir uma resposta é retornar o código de status 504.

Este 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 cada recurso de tempo limite que ele ilustra também tem suporte nos outros tipos de aplicativo.

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

Nota: Quando um aplicativo está sendo executado no modo de depuração, o middleware de tempo limite não é acionado. Esse comportamento é o mesmo que para Kestrel tempos limite. 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 app não faz com que timeouts comecem a ocorrer automaticamente. Os limites de tempo limite devem ser configurados explicitamente.

Configurar um endpoint ou uma página

Para aplicativos Minimal API, configure um endpoint para atingir o tempo limite chamando WithRequestTimeout ou aplicando o atributo [RequestTimeout], conforme mostrado no exemplo 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 [RequestTimeout] atributo ao método de ação ou à classe de controlador. Para apps Pages Razor, aplique o atributo à classe de página Razor.

Configurar vários endpoints ou páginas

Crie políticas nomeadas para especificar a configuração de tempo limite que se aplica a vários endpoints. 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 [RequestTimeout] atributo 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 aplica-se a endpoints para os quais não foi especificado um tempo limite. O código do endpoint a seguir verifica se houve timeout, embora não chame o método de extensão nem 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 RequestTimeoutPolicy classe tem uma propriedade que pode definir automaticamente o código de 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 RequestTimeoutPolicy classe tem uma WriteTimeoutResponse propriedade 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 [DisableRequestTimeout] atributo ou o método de DisableRequestTimeout extensão:

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 timeout que já tenha sido 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.

Consulte também