Intergiciel de délai d’expiration des requêtes dans ASP.NET Core

Par Tom Dykstra

Les applications peuvent appliquer de manière sélective des limites de délai d’expiration aux requêtes. Les serveurs ASP.NET Core ne le font pas par défaut, car les temps de traitement des requêtes varient considérablement d’un scénario à l’autre. Par exemple, WebSockets, des fichiers statiques et l’appel d’API coûteuses nécessitent chacun une limite de délai d’expiration différente. Par conséquent, ASP.NET Core fournit un intergiciel qui configure des délais d’expiration par point de terminaison ainsi qu’un délai d’expiration global.

Lorsqu’une limite de délai d’expiration est atteinte, un CancellationToken dans HttpContext.RequestAborted a la valeur de IsCancellationRequested définie sur true. Abort() n’est pas appelé automatiquement sur la requête, de sorte que l’application peut toujours produire une réponse de réussite ou d’échec. Le comportement par défaut si l’application ne gère pas l’exception et produit une réponse consiste à retourner un code d’état 504.

Cet article explique comment configurer l’intergiciel de délai d’expiration. L’intergiciel de délai d’expiration peut être utilisé dans tous les types d’applications ASP.NET Core : API minimale, API web avec contrôleurs, MVC et Razor Pages. L’exemple d’application est une API minimale, mais chaque délai d’expiration qu’elle illustre est également prise en charge dans les autres types d’application.

Les délais d’expiration des requêtes se trouvent dans l’espace de noms Microsoft.AspNetCore.Http.Timeouts.

Remarque : Lorsqu’une application s’exécute en mode débogage, le middleware du délai d’attente ne se déclenche pas. Ce comportement est le même que pour les délais d’expirationKestrel. Pour tester les délais d’expiration, exécutez l’application sans le débogueur attaché.

Ajouter l’intergiciel à une application

Ajoutez l’intergiciel de délais d’expiration de requêtes à la collection de services en appelant AddRequestTimeouts.

Ajoutez l’intergiciel au pipeline de traitement des demandes en appelant UseRequestTimeouts.

Remarque

  • Dans les applications qui appellent UseRouting explicitement, UseRequestTimeouts doit être appelé après UseRouting.

L’ajout de l’intergiciel à l’application ne commence pas automatiquement à déclencher des délais d’expiration. Les limites de délai d’expiration doivent être configurées de manière explicite.

Configurer un point de terminaison ou une page

Pour les applications API minimales, configurez le délai d’attente d’un point de terminaison en appelant WithRequestTimeout ou en appliquant l’attribut [RequestTimeout], comme illustré dans les exemples suivants :

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

Pour les applications avec des contrôleurs, appliquez l’attribut [RequestTimeout] pour la méthode d’action ou à la classe de contrôleur. Pour les applications Razor Pages, appliquez l’attribut à la classe de page Razor.

Configurer plusieurs points de terminaison ou pages

Créez des stratégies nommées pour spécifier la configuration du délai d’expiration qui s’applique à plusieurs points de terminaison. Ajoutez une stratégie en appelant AddPolicy :

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

Un délai d’expiration peut être spécifié pour un point de terminaison par nom de stratégie :

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

L’attribut [RequestTimeout] peut également être utilisé pour spécifier une stratégie nommée.

Définir une stratégie globale de délai d’expiration par défaut

Spécifiez une stratégie pour la configuration globale du délai d’expiration par défaut :

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

Le délai d’expiration par défaut s’applique aux points de terminaison qui n’ont pas de délai d’expiration spécifié. Le code de point de terminaison suivant recherche un délai d’expiration, bien qu’il n’appelle pas la méthode d’extension ni n’applique l’attribut. La configuration du délai d’expiration global s’applique, de sorte que le code recherche un délai d’expiration :

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.

Spécifier le code d’état dans une stratégie

La classe RequestTimeoutPolicy a une propriété qui peut définir automatiquement le code d’état lorsqu’un délai d’expiration est déclenché.

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.

Utiliser un délégué dans une stratégie

La classe RequestTimeoutPolicy a une propriété WriteTimeoutResponse qui peut être utilisée pour personnaliser la réponse lorsqu’un délai d’expiration est déclenché.

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.

Désactiver des délais d’expiration

Pour désactiver tous les délais d’expiration, y compris le délai d’expiration global par défaut, utilisez l’attribut [DisableRequestTimeout] ou la méthode d’extension 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.

Annuler un délai d’expiration

Pour annuler un délai d’expiration qui a déjà été démarré, utilisez la méthode DisableTimeout() sur IHttpRequestTimeoutFeature. Les délais d’expiration ne peuvent pas être annulés après leur expiration.

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.

Voir aussi