Компонент промежуточного ПО для тайм-аутов запросов в ASP.NET Core

Том Дайкстра

Приложения могут применять ограничения времени ожидания выборочно к запросам. ASP.NET основные серверы по умолчанию этого не делают, так как время обработки запросов зависит от сценария. Например, WebSockets, статические файлы и вызовы дорогостоящих API требуют другого ограничения времени ожидания. Поэтому ASP.NET Core предоставляет ПО промежуточного слоя, которое настраивает время ожидания для каждой конечной точки, а также глобальное время ожидания.

Если достигнут предел времени ожидания, для CancellationToken в HttpContext.RequestAborted значение IsCancellationRequested установлено в true. Abort() не вызывается автоматически при обработке запроса, поэтому приложение по-прежнему может вернуть успешный ответ или ответ об ошибке. Поведение по умолчанию, если приложение не обрабатывает исключение и создает ответ, является возврат кода состояния 504.

В этой статье рассказывается, как настроить промежуточный компонент тайм-аута. Промежуточное ПО для тайм-аута можно использовать во всех типах приложений ASP.NET Core: Minimal API, Web API с контроллерами, MVC и Razor Pages. Пример приложения представляет собой приложение Minimal API, однако все функции тайм-аута, которые в нём показаны, также поддерживаются и в других типах приложений.

Тайм-ауты запросов находятся в пространстве имен Microsoft.AspNetCore.Http.Timeouts.

Примечание: Когда приложение работает в режиме отладки, промежуточное ПО тайм-аута не срабатывает. Это поведение совпадает с Kestrel временем ожидания. Чтобы проверить время ожидания, запустите приложение без подключения отладчика.

Добавьте промежуточное ПО в приложение

Добавьте компонент промежуточного ПО для тайм-аутов запросов в коллекцию сервисов, вызвав AddRequestTimeouts.

Добавьте ПО промежуточного слоя в конвейер обработки запросов путем вызова UseRequestTimeouts.

Замечание

  • В приложениях, которые явно вызывают UseRouting, UseRequestTimeouts необходимо вызывать после UseRouting.

Добавление промежуточного ПО в приложение не приводит к автоматическому срабатыванию тайм-аутов. Ограничения времени ожидания должны быть явно настроены.

Настройка одной конечной точки или страницы

Для приложений API минимального размера настройте конечную точку для ожидания путем вызова WithRequestTimeoutили применения атрибута [RequestTimeout] , как показано в следующем примере:

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

Для приложений с контроллерами примените [RequestTimeout] атрибут к методу действия или классу контроллера. Для приложений Razor Pages примените этот атрибут к классу страницы Razor.

Настройка нескольких конечных точек или страниц

Создайте именованные политики , чтобы указать конфигурацию времени ожидания, которая применяется к нескольким конечным точкам. Добавление политики путем вызова AddPolicy:

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

Время ожидания можно указать для конечной точки по имени политики:

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

Атрибут [RequestTimeout] также можно использовать для указания именованной политики.

Настройка глобальной политики времени ожидания по умолчанию

Укажите политику для глобальной конфигурации времени ожидания по умолчанию:

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

Время ожидания по умолчанию применяется к конечным точкам, у которых нет указанного времени ожидания. Следующий код конечной точки проверяет время ожидания, хотя он не вызывает метод расширения или не применяет атрибут. Применяется глобальная конфигурация времени ожидания, поэтому код проверяет время ожидания:

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.

Указание кода состояния в политике

Класс RequestTimeoutPolicy имеет свойство, которое может автоматически задать код состояния при активации времени ожидания.

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.

Использование делегата в политике

Класс RequestTimeoutPolicy имеет WriteTimeoutResponse свойство, которое можно использовать для настройки ответа при активации времени ожидания.

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.

Отключение времени ожидания

Чтобы отключить все тайм-ауты, включая глобальное время ожидания по умолчанию, используйте [DisableRequestTimeout] атрибут или 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.

Отмена времени ожидания

Чтобы отменить тайм-аут, который уже был запущен, используйте метод DisableTimeout() объекта IHttpRequestTimeoutFeature. Тайм-ауты невозможно отменить после их истечения.

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.

См. также