Middleware de tiempo de espera de solicitudes en ASP.NET Core

Por Tom Dykstra

Las aplicaciones pueden aplicar límites de tiempo de espera de forma selectiva a las solicitudes. ASP.NET Core servidores no lo hacen de forma predeterminada, ya que los tiempos de procesamiento de solicitudes varían ampliamente por escenario. Por ejemplo, WebSockets, archivos estáticos y llamadas a API costosas requerirían un límite de tiempo de espera diferente. Por lo tanto, ASP.NET Core proporciona middleware que configura tiempos de espera por punto de conexión, así como un tiempo de espera global.

Cuando se alcanza un límite de tiempo de espera, un CancellationToken se ha HttpContext.RequestAborted establecido IsCancellationRequesteden true. Abort() no se llama automáticamente a en la solicitud, por lo que la aplicación puede seguir produciendo una respuesta correcta o errónea. El comportamiento predeterminado si la aplicación no controla la excepción y genera una respuesta es devolver el código de estado 504.

En este artículo se explica cómo configurar el middleware de tiempo de espera. El middleware de tiempo de espera puede utilizarse en todos los tipos de aplicaciones ASP.NET Core: Minimal API, Web API con controladores, MVC y Razor Pages. La aplicación de ejemplo es una API mínima, pero todas las funciones de tiempo de espera que ilustra también se admiten con los otros tipos de aplicaciones.

Los tiempos de espera de la solicitud se encuentran en el Microsoft.AspNetCore.Http.Timeouts espacio de nombres.

Nota: Cuando una aplicación se ejecuta en modo de depuración, el middleware del tiempo de espera no se desencadena. Los tiempos de espera de Kestrel. Para probar los tiempos de espera, ejecute la aplicación sin el depurador asociado.

Adición del middleware a la aplicación

Añade el middleware de tiempo de espera de solicitud a la colección de servicios llamando a AddRequestTimeouts.

Agregue el middleware a la canalización de procesamiento de solicitudes mediante una llamada a UseRequestTimeouts.

Nota:

  • En las aplicaciones que llaman explícitamente a UseRouting, UseRequestTimeouts se debe llamar a después deUseRouting.

Agregar el middleware a la aplicación no inicia automáticamente el desencadenamiento de tiempos de espera. Los límites de tiempo de espera deben configurarse explícitamente.

Configuración de un punto de conexión o página

En el caso de las aplicaciones de API mínimas, configure un punto de conexión para que se produzca un tiempo de espera llamando a WithRequestTimeout, o aplicando el atributo [RequestTimeout], como se muestra en el ejemplo siguiente:

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 aplicaciones con controladores, aplique el [RequestTimeout] atributo al método de acción o a la clase del controlador. Para Razor las aplicaciones Pages, aplique el atributo a la Razor clase de página.

Configuración de varios puntos de conexión o páginas

Cree directivas con nombre para especificar la configuración de tiempo de espera que se aplica a varios puntos de conexión. Agregue una directiva llamando a AddPolicy:

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

Se puede especificar un tiempo de espera para un punto de conexión por nombre de directiva:

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

El [RequestTimeout] atributo también se puede usar para especificar una directiva con nombre.

Establecimiento de la directiva de tiempo de espera predeterminada global

Especifique una política para la configuración global de tiempo de espera predeterminado:

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

El tiempo de espera predeterminado se aplica a los puntos de conexión que no tienen especificado un tiempo de espera. El código de punto de conexión siguiente comprueba si hay un tiempo de espera, aunque no llama al método de extensión ni aplica el atributo. Se aplica la configuración de tiempo de espera global, por lo que el código comprueba si hay un tiempo de espera:

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 el código de estado en una directiva

La RequestTimeoutPolicy clase tiene una propiedad que puede establecer automáticamente el código de estado cuando se desencadena un tiempo de espera.

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.

Uso de un delegado en una directiva

La RequestTimeoutPolicy clase tiene una WriteTimeoutResponse propiedad que se puede usar para personalizar la respuesta cuando se desencadena un tiempo de espera.

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.

Des habilitación de tiempos de espera

Para deshabilitar todos los tiempos de espera, incluido el tiempo de espera global predeterminado, use el [DisableRequestTimeout] atributo o el método de DisableRequestTimeout extensión:

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.

Cancelar un tiempo de espera

Para cancelar un tiempo de espera que ya se ha iniciado, use el DisableTimeout() método en IHttpRequestTimeoutFeature. Los tiempos de espera no se pueden cancelar después de que hayan expirado.

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 también