Middleware für Anforderungstimeouts in ASP.NET Core

Von Tom Dykstra

Apps können Timeoutlimits selektiv auf Anforderungen anwenden. Bei ASP.NET Core-Servern ist dies kein Standardverhalten, weil die Verarbeitungszeiten von Anforderungen je nach Szenario stark variieren. Beispielsweise erfordern WebSockets, statische Dateien und das Aufrufen ressourcenintensiver APIs jeweils ein anderes Timeoutlimit. Daher stellt ASP.NET Core Middleware bereit, die Timeouts pro Endpunkt sowie ein globales Timeout konfiguriert.

Bei Erreichen eines Timeoutlimits wird für ein CancellationToken in HttpContext.RequestAborted der IsCancellationRequested-Wert auf true festgelegt. Abort() wird nicht automatisch für die Anforderung aufgerufen, sodass die Anwendung möglicherweise trotzdem eine Erfolgs- oder Fehlerantwort erzeugt. Das Standardverhalten, wenn die App die Ausnahme nicht verarbeitet und keine Antwort erzeugt, besteht darin, Statuscode 504 zurückzugeben.

In diesem Artikel wird erläutert, wie die Middleware für Timeouts konfiguriert wird. Die Middleware für Timeouts kann in allen Arten von ASP.NET Core-Apps verwendet werden: Minimal-API, Web-API mit Controllern, MVC und Razor Pages. Die Beispiel-App ist eine Minimal-API, aber die darin demonstrierten Timeoutfeatures werden auch in den anderen App-Typen unterstützt.

Anforderungstimeouts befinden sich im Microsoft.AspNetCore.Http.Timeouts-Namespace.

Hinweis: Wenn eine App im Debugmodus ausgeführt wird, wird die Timeoutmiddleware nicht ausgelöst. Dieses Verhalten entspricht dem Verhalten in Kestrel-Timeouts. Führen Sie die App ohne angefügten Debugger aus, um Timeouts zu testen.

Hinzufügen der Middleware zur App

Fügen Sie die Middleware für Anforderungstimeouts der Dienstsammlung hinzu, indem Sie AddRequestTimeouts aufrufen.

Fügen Sie die Middleware zur Anforderungsverarbeitungspipeline hinzu, indem Sie UseRequestTimeouts aufrufen.

Hinweis

  • In Apps, die UseRouting explizit aufrufen, muss UseRequestTimeouts nach UseRouting aufgerufen werden.

Durch Hinzufügen der Middleware zur App wird nicht automatisch mit dem Auslösen von Timeouts begonnen. Timeoutlimits müssen explizit konfiguriert werden.

Konfigurieren eines Endpunkts oder einer Seite

Bei minimalen API-Apps konfigurieren Sie einen Endpunkt für Timeouts, indem Sie WithRequestTimeout aufrufen oder das [RequestTimeout]-Attribut anwenden, wie im folgenden Beispiel gezeigt:

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

Bei Apps mit Controllern wenden Sie das [RequestTimeout]-Attribut auf die Aktionsmethode oder Controllerklasse an. Bei Razor Pages-Apps wenden Sie das Attribut auf die Razor-Seitenklasse an.

Konfigurieren mehrerer Endpunkte oder Seiten

Erstellen Sie benannte Richtlinien, um eine Timeoutkonfiguration anzugeben, die für mehrere Endpunkte gilt. Fügen Sie eine Richtlinie hinzu, indem Sie AddPolicy aufrufen:

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

Ein Timeout für einen Endpunkt kann anhand des Richtliniennamens angegeben werden:

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

Das [RequestTimeout]-Attribut kann auch zum Angeben einer benannten Richtlinie verwendet werden.

Festlegen einer globalen Richtlinie für das Standardtimeout

Geben Sie eine Richtlinie für die globale Konfiguration eines Standardtimeouts an:

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

Das Standardtimeout gilt für Endpunkte, für die kein Timeout angegeben ist. Mit dem folgenden Endpunktcode wird auf ein Timeout geprüft, obwohl die Erweiterungsmethode nicht aufgerufen und das Attribut nicht angewendet wird. Die globale Timeoutkonfiguration gilt, sodass der Code auf ein Timeout prüft:

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.

Angeben des Statuscodes in einer Richtlinie

Die RequestTimeoutPolicy-Klasse verfügt über eine Eigenschaft, die den Statuscode automatisch festlegen kann, sobald ein Timeout ausgelöst wird.

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.

Verwenden eines Delegaten in einer Richtlinie

Die RequestTimeoutPolicy-Klasse verfügt über eine WriteTimeoutResponse-Eigenschaft, die bei Auslösen eines Timeouts verwendet werden kann, um die Antwort anzupassen.

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.

Deaktivieren von Timeouts

Um alle Timeouts zu deaktivieren, einschließlich des globalen Standardtimeouts, verwenden Sie das [DisableRequestTimeout]-Attribut oder die DisableRequestTimeout-Erweiterungsmethode:

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.

Abbrechen eines Timeouts

Um ein bereits gestartetes Timeout abzubrechen, verwenden Sie die DisableTimeout()-Methode für IHttpRequestTimeoutFeature. Timeouts können nicht abgebrochen werden, nachdem sie abgelaufen sind.

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.

Siehe auch