Обработка ошибок в ASP.NET Core

Note

Это не последняя версия этой статьи. В текущей версии см. версию .NET 10 этой статьи.

Warning

Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в политике поддержки .NET и .NET Core. В текущей версии см. версию .NET 10 этой статьи.

В этой статье рассматриваются основные методы обработки ошибок в веб-приложениях ASP.NET Core. См. также Обработка ошибок в API ASP.NET Core.

Инструкции Blazor по обработке ошибок, которые добавляют к рекомендациям в этой статье или заменяют их, см. в статье Обработка ошибок в Blazor приложениях ASP.NET Core.

Страница исключения для разработчиков

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

Приложения ASP.NET Core по умолчанию включают страницу исключений для разработчиков, если одновременно выполняются оба условия:

Приложения, созданные с использованием более ранних шаблонов, то есть с помощью WebHost.CreateDefaultBuilder, могут включить страницу исключений разработчика, вызвав app.UseDeveloperExceptionPage.

Warning

Не включите страницу исключений разработчика, если приложение не запущено в Development среде. Не делитесь подробными сведениями об исключениях публично при запуске приложения в рабочей среде. Дополнительные сведения о настройке сред см. в ASP.NET средах выполнения Core.

Страница исключений для разработчика может содержать следующие сведения об исключении и запросе:

  • Трассировка стека
  • параметры строки запроса (при наличии);
  • Cookies, если таковой есть
  • Headers
  • Метаданные конечной точки, если таковые есть

Страница исключений для разработчика не обязательно содержит какую-либо информацию. Используйте Ведение журнала для получения полных сведений об ошибке.

На следующем рисунке показана пример страницы исключений разработчика с анимацией для отображения вкладок и отображаемых сведений:

Страница исключений разработчика, анимированная для отображения каждой выбранной вкладки.

В ответ на запрос с заголовком страница исключений разработчика возвращает обычный Accept: text/plain текст вместо HTML. Рассмотрим пример.

Status: 500 Internal Server Error
Time: 9.39 msSize: 480 bytes
FormattedRawHeadersRequest
Body
text/plain; charset=utf-8, 480 bytes
System.InvalidOperationException: Sample Exception
   at WebApplicationMinimal.Program.<>c.<Main>b__0_0() in C:\Source\WebApplicationMinimal\Program.cs:line 12
   at lambda_method1(Closure, Object, HttpContext)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

HEADERS
=======
Accept: text/plain
Host: localhost:7267
traceparent: 00-0eab195ea19d07b90a46cd7d6bf2f

Страница обработчика исключений

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

  • Перехватывает и записывает в журнал необработанные исключения.
  • повторно выполняет запрос в альтернативном конвейере по указанному пути. Запрос не выполняется повторно, если запущен отклик. Созданный шаблоном код повторно выполняет запрос, используя путь /Error.

Warning

Если альтернативный конвейер вызывает собственное исключение, middleware обработки исключений повторно выбрасывает исходное исключение.

Так как это ПО промежуточного слоя может повторно выполнить конвейер запросов:

  • Промежуточное ПО должно поддерживать реентерабельность при повторном входе с тем же запросом. Обычно это означает либо очищать их состояние после вызова _next, либо кэшировать результаты их обработки в HttpContext, чтобы не выполнять её повторно. При работе с текстом запроса это означает буферизацию или кэширование результатов, таких как средство чтения форм.
  • Для перегрузки UseExceptionHandler(IApplicationBuilder, String) , используемой в шаблонах, изменяется только путь запроса, а данные маршрута очищаются. Данные запроса, такие как заголовки, метод и элементы, повторно используются без изменений.
  • Службы с ограниченной областью действия остаются неизменными.

В следующем примере UseExceptionHandler добавляет Middleware обработки исключений в не-Development средах.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Шаблон приложения Razor Pages предоставляет страницу ошибки (.cshtml) и класс PageModel (ErrorModel) в папке Pages. Для приложения MVC шаблон проекта содержит метод действия Error и представление ошибок для контроллера Home.

ПО промежуточного слоя обработки исключений повторно выполняет запрос, используя исходный метод HTTP. Если конечная точка обработчика ошибок ограничена определенным набором методов HTTP, она выполняется только для этих методов HTTP. Например, действие контроллера MVC, использующее атрибут [HttpGet], выполняется только для запросов GET. Чтобы гарантировать, что все запросы будут попадать на страницу пользовательской обработки ошибок, не ограничивайте их определённым набором HTTP-методов.

Для избирательного управления исключениями в зависимости от исходного метода HTTP:

  • Для Razor страниц создайте несколько методов-обработчиков. Например, используйте OnGet, чтобы обрабатывать исключения GET, и OnPost, чтобы обрабатывать исключения POST.
  • Для MVC примените атрибуты HTTP-команды к нескольким действиям. Например, используйте [HttpGet], чтобы обрабатывать исключения GET, и [HttpPost], чтобы обрабатывать исключения POST.

Чтобы разрешить пользователям, не прошедшим проверку подлинности, просматривать страницу пользовательской обработки ошибок, убедитесь, что она поддерживает анонимный доступ.

Откройте исключение

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

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string? ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();

        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "The file was not found.";
        }

        if (exceptionHandlerPathFeature?.Path == "/")
        {
            ExceptionMessage ??= string.Empty;
            ExceptionMessage += " Page: Home.";
        }
    }
}

Warning

Не передавайте клиентам конфиденциальную информацию об ошибках. Сохранение ошибок создает риски для безопасности.

Лямбда-обработчик исключений

Альтернативой пользовательской странице обработчика исключений является предоставление лямбда-функции для UseExceptionHandler. Использование лямбда-функции позволяет получить доступ к ошибке до возврата ответа.

В следующем коде для обработки исключений используется лямбда-выражение:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            // using static System.Net.Mime.MediaTypeNames;
            context.Response.ContentType = Text.Plain;

            await context.Response.WriteAsync("An exception was thrown.");

            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync(" The file was not found.");
            }

            if (exceptionHandlerPathFeature?.Path == "/")
            {
                await context.Response.WriteAsync(" Page: Home.");
            }
        });
    });

    app.UseHsts();
}

Другой способ использовать лямбда-код — задать код состояния на основе типа исключения, как показано в следующем примере:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(new ExceptionHandlerOptions
    {
        StatusCodeSelector = ex => ex is TimeoutException
            ? StatusCodes.Status503ServiceUnavailable
            : StatusCodes.Status500InternalServerError
    });
}

Warning

Не передавайте клиентам конфиденциальную информацию об ошибках. Сохранение ошибок создает риски для безопасности.

IExceptionHandler

IExceptionHandler — это интерфейс, предоставляющий разработчику механизм обратного вызова для обработки известных исключений в одном месте. Интерфейс содержит один метод, TryHandleAsyncкоторый получает HttpContext и Exception параметр.

IExceptionHandler реализации регистрируются путем вызова IServiceCollection.AddExceptionHandler<T>. Время существования экземпляра IExceptionHandler — одноэлементное. Можно добавить несколько реализаций, и они вызываются в порядке регистрации.

Промежуточное программное обеспечение для обработки исключений последовательно перебирает зарегистрированные обработчики исключений до тех пор, пока один из них не вернет true из TryHandleAsync, указывая на то, что исключение было обработано. Если обработчик исключений обрабатывает исключение, он может вернуть true, чтобы остановить обработку. Если исключение не обрабатывается ни одним обработчиком исключений, используются поведение по умолчанию и параметры промежуточного ПО.

Начиная с .NET 10, поведение по умолчанию заключается в подавлении выбросов диагностики, таких как журналы и метрики для обрабатываемых исключений (при TryHandleAsync возврате true). Это отличается от предыдущих версий (.NET версий 8 и 9), где диагностика всегда выдавалась независимо от того, обрабатывается ли исключение. Поведение по умолчанию можно изменить с помощью параметра SuppressDiagnosticsCallback.

В следующем примере показана реализация IExceptionHandler:

using Microsoft.AspNetCore.Diagnostics;

namespace ErrorHandlingSample
{
    public class CustomExceptionHandler : IExceptionHandler
    {
        private readonly ILogger<CustomExceptionHandler> logger;
        public CustomExceptionHandler(ILogger<CustomExceptionHandler> logger)
        {
            this.logger = logger;
        }
        public ValueTask<bool> TryHandleAsync(
            HttpContext httpContext,
            Exception exception,
            CancellationToken cancellationToken)
        {
            var exceptionMessage = exception.Message;
            logger.LogError(
                "Error Message: {exceptionMessage}, Time of occurrence {time}",
                exceptionMessage, DateTime.UtcNow);
            // Return false to continue with the default behavior
            // - or - return true to signal that this exception is handled
            return ValueTask.FromResult(false);
        }
    }
}

В следующем примере показано, как зарегистрировать реализацию IExceptionHandler для внедрения зависимостей:

using ErrorHandlingSample;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
builder.Services.AddExceptionHandler<CustomExceptionHandler>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// Remaining Program.cs code omitted for brevity

При выполнении предыдущего Development кода в среде:

  • Сначала вызывается CustomExceptionHandler для обработки исключения.
  • После регистрации исключения метод возвращает TryHandleAsync, поэтому отображается false.

В других средах:

  • Сначала вызывается CustomExceptionHandler для обработки исключения.
  • После регистрации исключения метод TryHandleAsync возвращает false, поэтому отображается страница /Error.

SuppressDiagnosticsCallback

Начиная с .NET 10, можно контролировать, записывает ли промежуточное ПО для обработки исключений диагностику для обрабатываемых исключений, при помощи настройки свойства SuppressDiagnosticsCallback на ExceptionHandlerOptions. Этот обратный вызов получает контекст исключения и позволяет определить, следует ли отключать диагностику на основе конкретного исключения или запроса.

Чтобы вернуться к поведению .NET 8 и 9, где диагностика всегда выполняется для обрабатываемых исключений, задайте обратный вызов, чтобы всегда возвращать false.

app.UseExceptionHandler(new ExceptionHandlerOptions
{
    SuppressDiagnosticsCallback = context => false
});

Вы также можете условно отключить диагностику на основе типа исключения или другого контекста:

app.UseExceptionHandler(new ExceptionHandlerOptions
{
    SuppressDiagnosticsCallback = context => context.Exception is ArgumentException
});

Если исключение не обрабатывается какой-либо IExceptionHandler реализацией (все обработчики возвращаются false из TryHandleAsync), управление возвращается к поведению по умолчанию и параметрам из промежуточного слоя, а диагностика создается в соответствии со стандартным поведением посредника.

UseStatusCodePages

По умолчанию приложение ASP.NET Core не предоставляет страницу для кодов состояния ошибок HTTP, таких как код 404 Not Found (не найдено). Когда в приложении устанавливается код состояния ошибки HTTP 400–599 без текста, возвращается код состояния и пустой текст ответа. Чтобы включить обработчики по умолчанию, возвращающие только текст для распространенных кодов состояния ошибки, вызовите UseStatusCodePages в Program.cs:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages();

Вызовите UseStatusCodePages до ПО промежуточного слоя для обработки запросов. Например, вызовите UseStatusCodePages до ПО промежуточной обработки статических файлов и ПО промежуточной обработки конечных точек.

Если UseStatusCodePages не используется, при переходе по URL-адресу без конечной точки возвращается зависящее от браузера сообщение об ошибке, в котором указывается, что конечная точка не найдена. При вызове метода UseStatusCodePages браузер вернет следующий ответ:

Status Code: 404; Not Found

UseStatusCodePages обычно не применяется в рабочей среде, так как возвращает сообщение, бесполезное для пользователей.

Note

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

UseStatusCodePages со строкой формата

Чтобы настроить тип содержимого и текст ответа, используйте перегрузку UseStatusCodePages, которая принимает тип содержимого и строку форматирования.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");

В предыдущем коде {0} служит заполнителем для кода ошибки.

UseStatusCodePages со строкой формата обычно не применяется в рабочей среде, так как возвращает сообщение, бесполезное для пользователей.

UseStatusCodePages с использованием лямбда-выражения

Чтобы указать пользовательский код обработки ошибок и записи ответа, используйте перегрузку UseStatusCodePages, которая принимает лямбда-выражение.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages(async statusCodeContext =>
{
    // using static System.Net.Mime.MediaTypeNames;
    statusCodeContext.HttpContext.Response.ContentType = Text.Plain;

    await statusCodeContext.HttpContext.Response.WriteAsync(
        $"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});

UseStatusCodePages с функцией Lambda обычно не используется в рабочей среде, так как возвращает сообщение, не представляющее пользы для пользователей.

UseStatusCodePagesWithRedirects

Метод расширения UseStatusCodePagesWithRedirects:

  • Отправляет клиенту код состояния 302 — Found.
  • Перенаправляет клиент в конечную точку обработки ошибок, указанную в шаблоне URL-адреса. Конечная точка обработки ошибок обычно отображает сведения об ошибке и возвращает код HTTP 200.
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");

Шаблон URL-адреса может содержать заполнитель {0} для кода состояния, как показано в предыдущем коде. Если шаблон URL-адреса начинается с ~ (тильды), ~ заменяется на PathBase приложения. При указании конечной точки в приложении создайте представление MVC или страницу Razor для конечной точки.

Этот метод обычно используется, если приложение:

  • Должно перенаправлять клиент в другую конечную точку, что обычно бывает в случаях, когда другое приложение обрабатывает ошибку. Для веб-приложений в адресной строке браузера клиента отображается конечная точка перенаправления.
  • Не следует сохранять и возвращать исходный код состояния в ответе первоначального перенаправления.

UseStatusCodePagesWithReExecute

Метод расширения UseStatusCodePagesWithReExecute:

  • Позволяет создать текст ответа путем повторного выполнения конвейера запросов с использованием другого пути.
  • Не изменяет код состояния до или после повторного выполнения конвейера.

Новое выполнение конвейера может изменить код состояния ответа, так как новый конвейер имеет полный контроль над кодом состояния. Если новый конвейер не изменяет код состояния, исходный код состояния будет отправлен клиенту.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");

Если указывается конечная точка в приложении, создайте представление MVC или страницу Razor для конечной точки.

Этот метод обычно используется, если приложение:

  • Обрабатывает запрос без перенаправления к другой конечной точке. Для веб-приложений в адресной строке браузера клиента отображается изначально запрошенная конечная точка.
  • Сохраняет и возвращает исходный код состояния с ответом.

Шаблон URL-адреса должен начинаться с символа / и может содержать заполнитель {0} для кода состояния. Чтобы передать код состояния в качестве параметра строки запроса, передайте второй аргумент в UseStatusCodePagesWithReExecute. Рассмотрим пример.

var app = builder.Build();  
app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");

Конечная точка, которая обрабатывает ошибку, может получать исходный URL-адрес, вызвавший ошибку, как показано в следующем примере:

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
    public int OriginalStatusCode { get; set; }

    public string? OriginalPathAndQuery { get; set; }

    public void OnGet(int statusCode)
    {
        OriginalStatusCode = statusCode;

        var statusCodeReExecuteFeature =
            HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

        if (statusCodeReExecuteFeature is not null)
        {
            OriginalPathAndQuery = $"{statusCodeReExecuteFeature.OriginalPathBase}"
                                    + $"{statusCodeReExecuteFeature.OriginalPath}"
                                    + $"{statusCodeReExecuteFeature.OriginalQueryString}";

        }
    }
}

Так как это ПО промежуточного слоя может повторно выполнить конвейер запросов:

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

Отключение страниц с кодами состояния

Чтобы отключить страницы кодов состояния для метода контроллера или действия MVC, используйте атрибут [SkipStatusCodePages].

Чтобы отключить страницы кодов состояния для конкретных запросов в методе обработчика Razor Pages или в контроллере MVC, используйте IStatusCodePagesFeature.

public void OnGet()
{
    var statusCodePagesFeature =
        HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature is not null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}

Код обработки исключений

Код на страницах обработки исключений также может создавать исключения. Рабочие страницы ошибок необходимо тщательно тестировать, чтобы они не создавали собственных исключений.

Заголовки ответа

После отправки заголовков для ответа происходит следующее:

  • Приложение не может изменить код состояния ответа.
  • Нельзя запустить страницу или обработчик исключений. Необходимо завершить ответ или прервать подключение.

Обработка исключений на сервере

Помимо логики обработки исключений в приложении, реализация HTTP-сервера также может обрабатывать некоторые исключения. Если сервер перехватывает исключение перед отправкой заголовков ответа, сервер отправляет 500 - Internal Server Error ответ без текста ответа. Если сервер перехватывает исключение после отправки заголовков ответа, он закрывает соединение. Запросы, не обработанные приложением, обрабатываются сервером. Все исключения, возникшие при обработке запроса серверов, обрабатываются с помощью механизма обработки исключений на сервере. Пользовательские страницы ошибок приложения, компоненты промежуточного ПО для обработки исключений и фильтры не влияют на это поведение.

Обработка исключений при запуске

Исключения, возникающие во время запуска приложения, могут обрабатываться только на уровне размещения. Узел можно настроить для перехвата ошибок при загрузке и записи подробных сведений об ошибках.

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

  • Уровень хостинга регистрирует в журнале критическое исключение.
  • Процесс dotnet аварийно завершается.
  • Если приложение запущено на HTTP-сервере Kestrel, страница со сведениями об ошибке не отображается.

При работе в службах IIS (или Службе приложений Azure) либо IIS Expressмодуль ASP.NET Core возвращает ошибку 502.5 Process Failure (ошибка процесса), если процесс невозможно запустить. Дополнительные сведения см. в статье Устранение неполадок с ASP.NET Core в Службе приложений Azure и IIS.

Страница ошибок базы данных

Фильтр исключений для страницы разработчика базы данных AddDatabaseDeveloperPageExceptionFilter перехватывает исключения, относящиеся к базе данных, которые могут быть устранены с помощью миграций Entity Framework Core. При возникновении этих исключений формируется HTML-ответ с подробными сведениями о возможных действиях для устранения проблемы. Эта страница включена только в Development среде. В следующем коде добавляется фильтр исключений для страницы разработчика базы данных:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();

Фильтры исключений

В приложениях MVC фильтры исключений можно настраивать как глобально, так и для отдельных контроллеров или действий. В приложениях Razor Pages они могут быть настроены глобально или для модели страницы. Эти фильтры обрабатывают все необработанные исключения, которые возникают во время выполнения действия контроллера или другого фильтра. Дополнительные сведения см. в статье Фильтры в ASP.NET Core.

Фильтры исключений полезны при перехвате исключений, которые возникают в действиях MVC. Однако эти фильтры не так гибки, как встроенное ПО промежуточного слоя для обработки исключенийUseExceptionHandler. Мы рекомендуем использовать UseExceptionHandler, если ошибки не нужно обрабатывать по-разному в зависимости от выбранного действия MVC.

Ошибки состояния модели

Сведения о том, как обрабатывать ошибки состояния модели, см. в статьях о привязке модели и проверке модели.

Сведения о проблеме

Сведения о проблеме — это не единственный формат ответа, описывающий ошибку API HTTP, однако они часто используются для сообщения об ошибках для API HTTP.

Служба сведений о проблеме IProblemDetailsService реализует интерфейс, который поддерживает создание сведений о проблеме в ASP.NET Core. Метод расширения AddProblemDetails(IServiceCollection) для IServiceCollection регистрирует реализацию IProblemDetailsService по умолчанию.

В приложениях ASP.NET Core следующее ПО промежуточной обработки создает HTTP-ответы со сведениями о проблеме при вызове AddProblemDetails, за исключением случаев, когда HTTP-заголовок запроса Accept не включает один из типов содержимого, поддерживаемых зарегистрированным IProblemDetailsWriter (по умолчанию: application/json):

  • ExceptionHandlerMiddleware: создает ответ с подробными сведениями о проблеме, если пользовательский обработчик не задан.
  • StatusCodePagesMiddleware: Создает ответ с подробными сведениями о проблеме по умолчанию.
  • DeveloperExceptionPageMiddleware: создает ответ с подробными сведениями о проблеме в среде разработки, если заголовок HTTP запроса Accept не содержит text/html.

Следующий код настраивает приложение на создание ответа с подробными сведениями о проблеме для всех ответов с ошибками HTTP-клиента и сервера, которые еще не содержат содержимого тела:

builder.Services.AddProblemDetails();

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();

В следующем разделе показано, как настроить тело ответа с подробными сведениями о проблеме.

Настройка сведений о проблеме

Автоматическое создание объекта ProblemDetails можно настроить с помощью любого из следующих параметров:

  1. Используйте ProblemDetailsOptions.CustomizeProblemDetails
  2. Использовать пользовательский IProblemDetailsWriter
  3. Вызовите IProblemDetailsService в промежуточном ПО

CustomizeProblemDetails операция

Сведения о созданной проблеме можно настроить с помощью CustomizeProblemDetails, и настройки применяются ко всем автоматически созданным сведениям о проблеме.

В следующем коде для задания ProblemDetailsOptions используется CustomizeProblemDetails:

builder.Services.AddProblemDetails(options =>
    options.CustomizeProblemDetails = ctx =>
            ctx.ProblemDetails.Extensions.Add("nodeId", Environment.MachineName));

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();

Например, результат конечной точки HTTP Status 400 Bad Request приводит к следующему телу ответа со сведениями о проблеме:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "nodeId": "my-machine-name"
}

Настраиваемый код IProblemDetailsWriter

Для расширенной настройки можно создать реализацию IProblemDetailsWriter.

public class SampleProblemDetailsWriter : IProblemDetailsWriter
{
    // Indicates that only responses with StatusCode == 400
    // are handled by this writer. All others are
    // handled by different registered writers if available.
    public bool CanWrite(ProblemDetailsContext context)
        => context.HttpContext.Response.StatusCode == 400;

    public ValueTask WriteAsync(ProblemDetailsContext context)
    {
        // Additional customizations.

        // Write to the response.
        var response = context.HttpContext.Response;
        return new ValueTask(response.WriteAsJsonAsync(context.ProblemDetails));
    }
}

Примечание: При использовании пользовательского IProblemDetailsWriter пользовательский IProblemDetailsWriter должен быть зарегистрирован перед вызовом AddRazorPages, AddControllers, AddControllersWithViews или AddMvc:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IProblemDetailsWriter, SampleProblemDetailsWriter>();

var app = builder.Build();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsWriter>() is
            { } problemDetailsService)
        {

            if (problemDetailsService.CanWrite(new ProblemDetailsContext() { HttpContext = context }))
            {
                (string Detail, string Type) details = mathErrorFeature.MathError switch
                {
                    MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                        "https://en.wikipedia.org/wiki/Division_by_zero"),
                    _ => ("Negative or complex numbers are not valid input.",
                        "https://en.wikipedia.org/wiki/Square_root")
                };

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                    {
                        Title = "Bad Input",
                        Detail = details.Detail,
                        Type = details.Type
                    }
                });
            }
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.DivisionByZeroError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.NegativeRadicandError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.Run();

Сведения о проблеме в промежуточном ПО

Альтернативный подход к использованию ProblemDetailsOptions с CustomizeProblemDetails заключается в настройке ProblemDetails в промежуточном ПО. Ответ с подробными сведениями о проблеме можно сформировать, вызвав IProblemDetailsService.WriteAsync:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStatusCodePages();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsService>() is
                                                           { } problemDetailsService)
        {
            (string Detail, string Type) details = mathErrorFeature.MathError switch
            {
                MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                "https://en.wikipedia.org/wiki/Division_by_zero"),
                _ => ("Negative or complex numbers are not valid input.", 
                "https://en.wikipedia.org/wiki/Square_root")
            };

            await problemDetailsService.WriteAsync(new ProblemDetailsContext
            {
                HttpContext = context,
                ProblemDetails =
                {
                    Title = "Bad Input",
                    Detail = details.Detail,
                    Type = details.Type
                }
            });
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.DivisionByZeroError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.NegativeRadicandError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.MapControllers();

app.Run();

В приведенном выше коде конечные точки минимального API /divide и /squareroot возвращают ожидаемый кастомизированный ответ на проблему при ошибочном вводе.

Эндпоинты контроллера API при некорректных входных данных возвращают ответ с описанием ошибки по умолчанию, а не пользовательский ответ с описанием ошибки. Стандартный ответ о проблеме возвращается, поскольку контроллер API записал в поток ответа сведения о проблеме для кодов состояния ошибок до вызова IProblemDetailsService.WriteAsync, и затем ответ не записывается повторно.

Следующий код ValuesController возвращает BadRequestResult, который записывает данные в поток ответа и поэтому не позволяет вернуть пользовательский ответ о проблеме.

[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // /api/values/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }

}

Следующий Values3Controller возвращает ControllerBase.Problem, поэтому возвращается ожидаемый пользовательский результат ошибки:

[Route("api/[controller]/[action]")]
[ApiController]
public class Values3Controller : ControllerBase
{
    // /api/values3/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Divison by zero is not defined.",
                type: "https://en.wikipedia.org/wiki/Division_by_zero",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values3/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Negative or complex numbers are not valid input.",
                type: "https://en.wikipedia.org/wiki/Square_root",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Math.Sqrt(radicand));
    }

}

Создать полезную нагрузку ProblemDetails для исключений

Рассмотрим следующее приложение:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapControllers();
app.Run();

В средах, отличных от разработки, при возникновении исключения ниже приведен стандартный ответ ProblemDetails, возвращаемый клиенту:

{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500,"traceId":"00-b644<snip>-00"
}

Для большинства приложений предыдущий код — это все, что необходимо для исключений. Однако в следующем разделе показано, как получить более подробные ответы на проблемы.

Альтернативой пользовательской странице обработчика исключений является предоставление лямбда-функции для UseExceptionHandler. Использование лямбда-выражения позволяет получить доступ к ошибке и записать в ответ сведения о проблеме с помощью IProblemDetailsService.WriteAsync:

using Microsoft.AspNetCore.Diagnostics;
using static System.Net.Mime.MediaTypeNames;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = Text.Plain;

            var title = "Bad Input";
            var detail = "Invalid input";
            var type = "https://errors.example.com/badInput";

            if (context.RequestServices.GetService<IProblemDetailsService>() is
                { } problemDetailsService)
            {
                var exceptionHandlerFeature =
               context.Features.Get<IExceptionHandlerFeature>();

                var exceptionType = exceptionHandlerFeature?.Error;
                if (exceptionType != null &&
                   exceptionType.Message.Contains("infinity"))
                {
                    title = "Argument exception";
                    detail = "Invalid input";
                    type = "https://errors.example.com/argumentException";
                }

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                {
                    Title = title,
                    Detail = detail,
                    Type = type
                }
                });
            }
        });
    });
}

app.MapControllers();
app.Run();

Warning

Не передавайте клиентам конфиденциальную информацию об ошибках. Сохранение ошибок создает риски для безопасности.

Альтернативный подход к созданию сведений о проблеме — использовать сторонний пакет NuGet Hellang.Middleware.ProblemDetails , который можно использовать для сопоставления исключений и ошибок клиента с сведениями о проблеме.

Дополнительные ресурсы

В этой статье рассматриваются основные методы обработки ошибок в веб-приложениях ASP.NET Core. См. также Обработка ошибок в API ASP.NET Core.

Инструкции Blazor по обработке ошибок, которые добавляют к рекомендациям в этой статье или заменяют их, см. в статье Обработка ошибок в Blazor приложениях ASP.NET Core.

Страница исключения для разработчиков

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

Приложения ASP.NET Core по умолчанию включают страницу исключений для разработчиков, если одновременно выполняются оба условия:

Приложения, созданные с использованием более ранних шаблонов, то есть с помощью WebHost.CreateDefaultBuilder, могут включить страницу исключений разработчика, вызвав app.UseDeveloperExceptionPage.

Warning

Не включите страницу исключений разработчика, если приложение не запущено в Development среде. Не делитесь подробными сведениями об исключениях публично при запуске приложения в рабочей среде. Дополнительные сведения о настройке сред см. в ASP.NET средах выполнения Core.

Страница исключений для разработчика может содержать следующие сведения об исключении и запросе:

  • Трассировка стека
  • параметры строки запроса (при наличии);
  • Cookies, если таковой есть
  • Headers
  • Метаданные конечной точки, если таковые есть

Страница исключений для разработчика не обязательно содержит какую-либо информацию. Используйте Ведение журнала для получения полных сведений об ошибке.

На следующем рисунке показана пример страницы исключений разработчика с анимацией для отображения вкладок и отображаемых сведений:

Страница исключений разработчика, анимированная для отображения каждой выбранной вкладки.

В ответ на запрос с заголовком страница исключений разработчика возвращает обычный Accept: text/plain текст вместо HTML. Рассмотрим пример.

Status: 500 Internal Server Error
Time: 9.39 msSize: 480 bytes
FormattedRawHeadersRequest
Body
text/plain; charset=utf-8, 480 bytes
System.InvalidOperationException: Sample Exception
   at WebApplicationMinimal.Program.<>c.<Main>b__0_0() in C:\Source\WebApplicationMinimal\Program.cs:line 12
   at lambda_method1(Closure, Object, HttpContext)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

HEADERS
=======
Accept: text/plain
Host: localhost:7267
traceparent: 00-0eab195ea19d07b90a46cd7d6bf2f

Страница обработчика исключений

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

  • Перехватывает и записывает в журнал необработанные исключения.
  • повторно выполняет запрос в альтернативном конвейере по указанному пути. Запрос не выполняется повторно, если запущен отклик. Созданный шаблоном код повторно выполняет запрос, используя путь /Error.

Warning

Если альтернативный конвейер вызывает собственное исключение, middleware обработки исключений повторно выбрасывает исходное исключение.

Так как это ПО промежуточного слоя может повторно выполнить конвейер запросов:

  • Промежуточное ПО должно поддерживать реентерабельность при повторном входе с тем же запросом. Обычно это означает либо очищать их состояние после вызова _next, либо кэшировать результаты их обработки в HttpContext, чтобы не выполнять её повторно. При работе с текстом запроса это означает буферизацию или кэширование результатов, таких как средство чтения форм.
  • Для перегрузки UseExceptionHandler(IApplicationBuilder, String) , используемой в шаблонах, изменяется только путь запроса, а данные маршрута очищаются. Данные запроса, такие как заголовки, метод и элементы, повторно используются без изменений.
  • Службы с ограниченной областью действия остаются неизменными.

В следующем примере UseExceptionHandler добавляет Middleware обработки исключений в не-Development средах.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Шаблон приложения Razor Pages предоставляет страницу ошибки (.cshtml) и класс PageModel (ErrorModel) в папке Pages. Для приложения MVC шаблон проекта содержит метод действия Error и представление ошибок для контроллера Home.

ПО промежуточного слоя обработки исключений повторно выполняет запрос, используя исходный метод HTTP. Если конечная точка обработчика ошибок ограничена определенным набором методов HTTP, она выполняется только для этих методов HTTP. Например, действие контроллера MVC, использующее атрибут [HttpGet], выполняется только для запросов GET. Чтобы гарантировать, что все запросы будут попадать на страницу пользовательской обработки ошибок, не ограничивайте их определённым набором HTTP-методов.

Для избирательного управления исключениями в зависимости от исходного метода HTTP:

  • Для Razor страниц создайте несколько методов-обработчиков. Например, используйте OnGet, чтобы обрабатывать исключения GET, и OnPost, чтобы обрабатывать исключения POST.
  • Для MVC примените атрибуты HTTP-команды к нескольким действиям. Например, используйте [HttpGet], чтобы обрабатывать исключения GET, и [HttpPost], чтобы обрабатывать исключения POST.

Чтобы разрешить пользователям, не прошедшим проверку подлинности, просматривать страницу пользовательской обработки ошибок, убедитесь, что она поддерживает анонимный доступ.

Откройте исключение

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

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string? ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();

        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "The file was not found.";
        }

        if (exceptionHandlerPathFeature?.Path == "/")
        {
            ExceptionMessage ??= string.Empty;
            ExceptionMessage += " Page: Home.";
        }
    }
}

Warning

Не передавайте клиентам конфиденциальную информацию об ошибках. Сохранение ошибок создает риски для безопасности.

Лямбда-обработчик исключений

Альтернативой пользовательской странице обработчика исключений является предоставление лямбда-функции для UseExceptionHandler. Использование лямбда-функции позволяет получить доступ к ошибке до возврата ответа.

В следующем коде для обработки исключений используется лямбда-выражение:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            // using static System.Net.Mime.MediaTypeNames;
            context.Response.ContentType = Text.Plain;

            await context.Response.WriteAsync("An exception was thrown.");

            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync(" The file was not found.");
            }

            if (exceptionHandlerPathFeature?.Path == "/")
            {
                await context.Response.WriteAsync(" Page: Home.");
            }
        });
    });

    app.UseHsts();
}

Другой способ использовать лямбда-код — задать код состояния на основе типа исключения, как показано в следующем примере:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(new ExceptionHandlerOptions
    {
        StatusCodeSelector = ex => ex is TimeoutException
            ? StatusCodes.Status503ServiceUnavailable
            : StatusCodes.Status500InternalServerError
    });
}

Warning

Не передавайте клиентам конфиденциальную информацию об ошибках. Сохранение ошибок создает риски для безопасности.

IExceptionHandler

IExceptionHandler — это интерфейс, предоставляющий разработчику механизм обратного вызова для обработки известных исключений в одном месте.

IExceptionHandler реализации регистрируются путем вызова IServiceCollection.AddExceptionHandler<T>. Время существования экземпляра IExceptionHandler — одноэлементное. Можно добавить несколько реализаций, и они вызываются в порядке регистрации.

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

В следующем примере показана реализация IExceptionHandler:

using Microsoft.AspNetCore.Diagnostics;

namespace ErrorHandlingSample
{
    public class CustomExceptionHandler : IExceptionHandler
    {
        private readonly ILogger<CustomExceptionHandler> logger;
        public CustomExceptionHandler(ILogger<CustomExceptionHandler> logger)
        {
            this.logger = logger;
        }
        public ValueTask<bool> TryHandleAsync(
            HttpContext httpContext,
            Exception exception,
            CancellationToken cancellationToken)
        {
            var exceptionMessage = exception.Message;
            logger.LogError(
                "Error Message: {exceptionMessage}, Time of occurrence {time}",
                exceptionMessage, DateTime.UtcNow);
            // Return false to continue with the default behavior
            // - or - return true to signal that this exception is handled
            return ValueTask.FromResult(false);
        }
    }
}

В следующем примере показано, как зарегистрировать реализацию IExceptionHandler для внедрения зависимостей:

using ErrorHandlingSample;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
builder.Services.AddExceptionHandler<CustomExceptionHandler>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// Remaining Program.cs code omitted for brevity

При выполнении предыдущего Development кода в среде:

  • Сначала вызывается CustomExceptionHandler для обработки исключения.
  • После регистрации исключения метод возвращает TryHandleAsync, поэтому отображается false.

В других средах:

  • Сначала вызывается CustomExceptionHandler для обработки исключения.
  • После регистрации исключения метод TryHandleAsync возвращает false, поэтому отображается страница /Error.

UseStatusCodePages

По умолчанию приложение ASP.NET Core не предоставляет страницу для кодов состояния ошибок HTTP, таких как код 404 Not Found (не найдено). Когда в приложении устанавливается код состояния ошибки HTTP 400–599 без текста, возвращается код состояния и пустой текст ответа. Чтобы включить обработчики по умолчанию, возвращающие только текст для распространенных кодов состояния ошибки, вызовите UseStatusCodePages в Program.cs:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages();

Вызовите UseStatusCodePages до ПО промежуточного слоя для обработки запросов. Например, вызовите UseStatusCodePages до ПО промежуточной обработки статических файлов и ПО промежуточной обработки конечных точек.

Если UseStatusCodePages не используется, при переходе по URL-адресу без конечной точки возвращается зависящее от браузера сообщение об ошибке, в котором указывается, что конечная точка не найдена. При вызове метода UseStatusCodePages браузер вернет следующий ответ:

Status Code: 404; Not Found

UseStatusCodePages обычно не применяется в рабочей среде, так как возвращает сообщение, бесполезное для пользователей.

Note

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

UseStatusCodePages со строкой формата

Чтобы настроить тип содержимого и текст ответа, используйте перегрузку UseStatusCodePages, которая принимает тип содержимого и строку форматирования.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");

В предыдущем коде {0} служит заполнителем для кода ошибки.

UseStatusCodePages со строкой формата обычно не применяется в рабочей среде, так как возвращает сообщение, бесполезное для пользователей.

UseStatusCodePages с использованием лямбда-выражения

Чтобы указать пользовательский код обработки ошибок и записи ответа, используйте перегрузку UseStatusCodePages, которая принимает лямбда-выражение.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages(async statusCodeContext =>
{
    // using static System.Net.Mime.MediaTypeNames;
    statusCodeContext.HttpContext.Response.ContentType = Text.Plain;

    await statusCodeContext.HttpContext.Response.WriteAsync(
        $"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});

UseStatusCodePages с функцией Lambda обычно не используется в рабочей среде, так как возвращает сообщение, не представляющее пользы для пользователей.

UseStatusCodePagesWithRedirects

Метод расширения UseStatusCodePagesWithRedirects:

  • Отправляет клиенту код состояния 302 — Found.
  • Перенаправляет клиент в конечную точку обработки ошибок, указанную в шаблоне URL-адреса. Конечная точка обработки ошибок обычно отображает сведения об ошибке и возвращает код HTTP 200.
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");

Шаблон URL-адреса может содержать заполнитель {0} для кода состояния, как показано в предыдущем коде. Если шаблон URL-адреса начинается с ~ (тильды), ~ заменяется на PathBase приложения. При указании конечной точки в приложении создайте представление MVC или страницу Razor для конечной точки.

Этот метод обычно используется, если приложение:

  • Должно перенаправлять клиент в другую конечную точку, что обычно бывает в случаях, когда другое приложение обрабатывает ошибку. Для веб-приложений в адресной строке браузера клиента отображается конечная точка перенаправления.
  • Не следует сохранять и возвращать исходный код состояния в ответе первоначального перенаправления.

UseStatusCodePagesWithReExecute

Метод расширения UseStatusCodePagesWithReExecute:

  • Позволяет создать текст ответа путем повторного выполнения конвейера запросов с использованием другого пути.
  • Не изменяет код состояния до или после повторного выполнения конвейера.

Новое выполнение конвейера может изменить код состояния ответа, так как новый конвейер имеет полный контроль над кодом состояния. Если новый конвейер не изменяет код состояния, исходный код состояния будет отправлен клиенту.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");

Если указывается конечная точка в приложении, создайте представление MVC или страницу Razor для конечной точки.

Этот метод обычно используется, если приложение:

  • Обрабатывает запрос без перенаправления к другой конечной точке. Для веб-приложений в адресной строке браузера клиента отображается изначально запрошенная конечная точка.
  • Сохраняет и возвращает исходный код состояния с ответом.

Шаблон URL-адреса должен начинаться с символа / и может содержать заполнитель {0} для кода состояния. Чтобы передать код состояния в качестве параметра строки запроса, передайте второй аргумент в UseStatusCodePagesWithReExecute. Рассмотрим пример.

var app = builder.Build();  
app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");

Конечная точка, которая обрабатывает ошибку, может получать исходный URL-адрес, вызвавший ошибку, как показано в следующем примере:

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
    public int OriginalStatusCode { get; set; }

    public string? OriginalPathAndQuery { get; set; }

    public void OnGet(int statusCode)
    {
        OriginalStatusCode = statusCode;

        var statusCodeReExecuteFeature =
            HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

        if (statusCodeReExecuteFeature is not null)
        {
            OriginalPathAndQuery = $"{statusCodeReExecuteFeature.OriginalPathBase}"
                                    + $"{statusCodeReExecuteFeature.OriginalPath}"
                                    + $"{statusCodeReExecuteFeature.OriginalQueryString}";

        }
    }
}

Так как это ПО промежуточного слоя может повторно выполнить конвейер запросов:

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

Отключение страниц с кодами состояния

Чтобы отключить страницы кодов состояния для метода контроллера или действия MVC, используйте атрибут [SkipStatusCodePages].

Чтобы отключить страницы кодов состояния для конкретных запросов в методе обработчика Razor Pages или в контроллере MVC, используйте IStatusCodePagesFeature.

public void OnGet()
{
    var statusCodePagesFeature =
        HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature is not null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}

Код обработки исключений

Код на страницах обработки исключений также может создавать исключения. Рабочие страницы ошибок необходимо тщательно тестировать, чтобы они не создавали собственных исключений.

Заголовки ответа

После отправки заголовков для ответа происходит следующее:

  • Приложение не может изменить код состояния ответа.
  • Нельзя запустить страницу или обработчик исключений. Необходимо завершить ответ или прервать подключение.

Обработка исключений на сервере

Помимо логики обработки исключений в приложении, реализация HTTP-сервера также может обрабатывать некоторые исключения. Если сервер перехватывает исключение перед отправкой заголовков ответа, сервер отправляет 500 - Internal Server Error ответ без текста ответа. Если сервер перехватывает исключение после отправки заголовков ответа, он закрывает соединение. Запросы, не обработанные приложением, обрабатываются сервером. Все исключения, возникшие при обработке запроса серверов, обрабатываются с помощью механизма обработки исключений на сервере. Пользовательские страницы ошибок приложения, компоненты промежуточного ПО для обработки исключений и фильтры не влияют на это поведение.

Обработка исключений при запуске

Исключения, возникающие во время запуска приложения, могут обрабатываться только на уровне размещения. Узел можно настроить для перехвата ошибок при загрузке и записи подробных сведений об ошибках.

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

  • Уровень хостинга регистрирует в журнале критическое исключение.
  • Процесс dotnet аварийно завершается.
  • Если приложение запущено на HTTP-сервере Kestrel, страница со сведениями об ошибке не отображается.

При работе в службах IIS (или Службе приложений Azure) либо IIS Expressмодуль ASP.NET Core возвращает ошибку 502.5 Process Failure (ошибка процесса), если процесс невозможно запустить. Дополнительные сведения см. в статье Устранение неполадок с ASP.NET Core в Службе приложений Azure и IIS.

Страница ошибок базы данных

Фильтр исключений для страницы разработчика базы данных AddDatabaseDeveloperPageExceptionFilter перехватывает исключения, относящиеся к базе данных, которые могут быть устранены с помощью миграций Entity Framework Core. При возникновении этих исключений формируется HTML-ответ с подробными сведениями о возможных действиях для устранения проблемы. Эта страница включена только в Development среде. В следующем коде добавляется фильтр исключений для страницы разработчика базы данных:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();

Фильтры исключений

В приложениях MVC фильтры исключений можно настраивать как глобально, так и для отдельных контроллеров или действий. В приложениях Razor Pages они могут быть настроены глобально или для модели страницы. Эти фильтры обрабатывают все необработанные исключения, которые возникают во время выполнения действия контроллера или другого фильтра. Дополнительные сведения см. в статье Фильтры в ASP.NET Core.

Фильтры исключений полезны при перехвате исключений, которые возникают в действиях MVC. Однако эти фильтры не так гибки, как встроенное ПО промежуточного слоя для обработки исключенийUseExceptionHandler. Мы рекомендуем использовать UseExceptionHandler, если ошибки не нужно обрабатывать по-разному в зависимости от выбранного действия MVC.

Ошибки состояния модели

Сведения о том, как обрабатывать ошибки состояния модели, см. в статьях о привязке модели и проверке модели.

Сведения о проблеме

Сведения о проблеме — это не единственный формат ответа, описывающий ошибку API HTTP, однако они часто используются для сообщения об ошибках для API HTTP.

Служба сведений о проблеме IProblemDetailsService реализует интерфейс, который поддерживает создание сведений о проблеме в ASP.NET Core. Метод расширения AddProblemDetails(IServiceCollection) для IServiceCollection регистрирует реализацию IProblemDetailsService по умолчанию.

В приложениях ASP.NET Core следующее ПО промежуточной обработки создает HTTP-ответы со сведениями о проблеме при вызове AddProblemDetails, за исключением случаев, когда HTTP-заголовок запроса Accept не включает один из типов содержимого, поддерживаемых зарегистрированным IProblemDetailsWriter (по умолчанию: application/json):

  • ExceptionHandlerMiddleware: создает ответ с подробными сведениями о проблеме, если пользовательский обработчик не задан.
  • StatusCodePagesMiddleware: Создает ответ с подробными сведениями о проблеме по умолчанию.
  • DeveloperExceptionPageMiddleware: создает ответ с подробными сведениями о проблеме в среде разработки, если заголовок HTTP запроса Accept не содержит text/html.

Следующий код настраивает приложение на создание ответа с подробными сведениями о проблеме для всех ответов с ошибками HTTP-клиента и сервера, которые еще не содержат содержимого тела:

builder.Services.AddProblemDetails();

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();

В следующем разделе показано, как настроить тело ответа с подробными сведениями о проблеме.

Настройка сведений о проблеме

Автоматическое создание объекта ProblemDetails можно настроить с помощью любого из следующих параметров:

  1. Используйте ProblemDetailsOptions.CustomizeProblemDetails
  2. Использовать пользовательский IProblemDetailsWriter
  3. Вызовите IProblemDetailsService в промежуточном ПО

CustomizeProblemDetails операция

Сведения о созданной проблеме можно настроить с помощью CustomizeProblemDetails, и настройки применяются ко всем автоматически созданным сведениям о проблеме.

В следующем коде для задания ProblemDetailsOptions используется CustomizeProblemDetails:

builder.Services.AddProblemDetails(options =>
    options.CustomizeProblemDetails = ctx =>
            ctx.ProblemDetails.Extensions.Add("nodeId", Environment.MachineName));

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();

Например, результат конечной точки HTTP Status 400 Bad Request приводит к следующему телу ответа со сведениями о проблеме:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "nodeId": "my-machine-name"
}

Настраиваемый код IProblemDetailsWriter

Для расширенной настройки можно создать реализацию IProblemDetailsWriter.

public class SampleProblemDetailsWriter : IProblemDetailsWriter
{
    // Indicates that only responses with StatusCode == 400
    // are handled by this writer. All others are
    // handled by different registered writers if available.
    public bool CanWrite(ProblemDetailsContext context)
        => context.HttpContext.Response.StatusCode == 400;

    public ValueTask WriteAsync(ProblemDetailsContext context)
    {
        // Additional customizations.

        // Write to the response.
        var response = context.HttpContext.Response;
        return new ValueTask(response.WriteAsJsonAsync(context.ProblemDetails));
    }
}

Примечание: При использовании пользовательского IProblemDetailsWriter пользовательский IProblemDetailsWriter должен быть зарегистрирован перед вызовом AddRazorPages, AddControllers, AddControllersWithViews или AddMvc:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IProblemDetailsWriter, SampleProblemDetailsWriter>();

var app = builder.Build();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsWriter>() is
            { } problemDetailsService)
        {

            if (problemDetailsService.CanWrite(new ProblemDetailsContext() { HttpContext = context }))
            {
                (string Detail, string Type) details = mathErrorFeature.MathError switch
                {
                    MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                        "https://en.wikipedia.org/wiki/Division_by_zero"),
                    _ => ("Negative or complex numbers are not valid input.",
                        "https://en.wikipedia.org/wiki/Square_root")
                };

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                    {
                        Title = "Bad Input",
                        Detail = details.Detail,
                        Type = details.Type
                    }
                });
            }
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.DivisionByZeroError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.NegativeRadicandError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.Run();

Подробности проблемы в промежуточном ПО

Альтернативный подход к использованию ProblemDetailsOptions с CustomizeProblemDetails заключается в настройке ProblemDetails в промежуточном ПО. Ответ с подробными сведениями о проблеме можно сформировать, вызвав IProblemDetailsService.WriteAsync:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStatusCodePages();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsService>() is
                                                           { } problemDetailsService)
        {
            (string Detail, string Type) details = mathErrorFeature.MathError switch
            {
                MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                "https://en.wikipedia.org/wiki/Division_by_zero"),
                _ => ("Negative or complex numbers are not valid input.", 
                "https://en.wikipedia.org/wiki/Square_root")
            };

            await problemDetailsService.WriteAsync(new ProblemDetailsContext
            {
                HttpContext = context,
                ProblemDetails =
                {
                    Title = "Bad Input",
                    Detail = details.Detail,
                    Type = details.Type
                }
            });
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.DivisionByZeroError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.NegativeRadicandError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.MapControllers();

app.Run();

В приведенном выше коде конечные точки минимального API /divide и /squareroot возвращают ожидаемый кастомизированный ответ на проблему при ошибочном вводе.

Эндпоинты контроллера API при некорректных входных данных возвращают ответ с описанием ошибки по умолчанию, а не пользовательский ответ с описанием ошибки. Стандартный ответ о проблеме возвращается, поскольку контроллер API записал в поток ответа сведения о проблеме для кодов состояния ошибок до вызова IProblemDetailsService.WriteAsync, и затем ответ не записывается повторно.

Следующий код ValuesController возвращает BadRequestResult, который записывает данные в поток ответа и поэтому не позволяет вернуть пользовательский ответ о проблеме.

[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // /api/values/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }

}

Следующий Values3Controller возвращает ControllerBase.Problem, поэтому возвращается ожидаемый пользовательский результат ошибки:

[Route("api/[controller]/[action]")]
[ApiController]
public class Values3Controller : ControllerBase
{
    // /api/values3/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Divison by zero is not defined.",
                type: "https://en.wikipedia.org/wiki/Division_by_zero",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values3/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Negative or complex numbers are not valid input.",
                type: "https://en.wikipedia.org/wiki/Square_root",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Math.Sqrt(radicand));
    }

}

Создать полезную нагрузку ProblemDetails для исключений

Рассмотрим следующее приложение:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapControllers();
app.Run();

В средах, отличных от разработки, при возникновении исключения ниже приведен стандартный ответ ProblemDetails, возвращаемый клиенту:

{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500,"traceId":"00-b644<snip>-00"
}

Для большинства приложений предыдущий код — это все, что необходимо для исключений. Однако в следующем разделе показано, как получить более подробные ответы на проблемы.

Альтернативой пользовательской странице обработчика исключений является предоставление лямбда-функции для UseExceptionHandler. Использование лямбда-выражения позволяет получить доступ к ошибке и записать в ответ сведения о проблеме с помощью IProblemDetailsService.WriteAsync:

using Microsoft.AspNetCore.Diagnostics;
using static System.Net.Mime.MediaTypeNames;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = Text.Plain;

            var title = "Bad Input";
            var detail = "Invalid input";
            var type = "https://errors.example.com/badInput";

            if (context.RequestServices.GetService<IProblemDetailsService>() is
                { } problemDetailsService)
            {
                var exceptionHandlerFeature =
               context.Features.Get<IExceptionHandlerFeature>();

                var exceptionType = exceptionHandlerFeature?.Error;
                if (exceptionType != null &&
                   exceptionType.Message.Contains("infinity"))
                {
                    title = "Argument exception";
                    detail = "Invalid input";
                    type = "https://errors.example.com/argumentException";
                }

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                {
                    Title = title,
                    Detail = detail,
                    Type = type
                }
                });
            }
        });
    });
}

app.MapControllers();
app.Run();

Warning

Не передавайте клиентам конфиденциальную информацию об ошибках. Сохранение ошибок создает риски для безопасности.

Альтернативный подход к созданию сведений о проблеме — использовать сторонний пакет NuGet Hellang.Middleware.ProblemDetails , который можно использовать для сопоставления исключений и ошибок клиента с сведениями о проблеме.

Дополнительные ресурсы

Автор: Том Дикстра (Tom Dykstra)

В этой статье рассматриваются основные методы обработки ошибок в веб-приложениях ASP.NET Core. См. также Обработка ошибок в API ASP.NET Core.

Страница исключения для разработчиков

Страница исключений для разработчика содержит подробные сведения о необработанных исключениях запросов. Приложения ASP.NET Core по умолчанию включают страницу исключений для разработчиков, если одновременно выполняются оба условия:

  • Запуск в Development среде.
  • Приложение создано с использованием текущих шаблонов, то есть с помощью WebApplication.CreateBuilder. Приложения, созданные с помощью WebHost.CreateDefaultBuilder, должны поддерживать страницу исключений разработчика путем вызова app.UseDeveloperExceptionPage и Configure.

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

Подробные сведения об исключении не должны отображаться публично при запуске приложения в Production среде. Дополнительные сведения о настройке сред см. в ASP.NET средах выполнения Core.

Страница исключений для разработчика может содержать следующие сведения об исключении и запросе:

  • Трассировка стека
  • параметры строки запроса (при наличии);
  • Cookies, если таковой есть
  • Headers

Страница исключений для разработчика не обязательно содержит какую-либо информацию. Используйте Ведение журнала для получения полных сведений об ошибке.

Страница обработчика исключений

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

  • Перехватывает и записывает в журнал необработанные исключения.
  • повторно выполняет запрос в альтернативном конвейере по указанному пути. Запрос не выполняется повторно, если запущен отклик. Созданный шаблоном код повторно выполняет запрос, используя путь /Error.

Warning

Если альтернативный конвейер вызывает собственное исключение, middleware обработки исключений повторно выбрасывает исходное исключение.

Так как это ПО промежуточного слоя может повторно выполнить конвейер запросов:

  • Промежуточное ПО должно поддерживать реентерабельность при повторном входе с тем же запросом. Обычно это означает либо очищать их состояние после вызова _next, либо кэшировать результаты их обработки в HttpContext, чтобы не выполнять её повторно. При работе с текстом запроса это означает буферизацию или кэширование результатов, таких как средство чтения форм.
  • Для перегрузки UseExceptionHandler(IApplicationBuilder, String) , используемой в шаблонах, изменяется только путь запроса, а данные маршрута очищаются. Данные запроса, такие как заголовки, метод и элементы, повторно используются без изменений.
  • Службы с ограниченной областью действия остаются неизменными.

В следующем примере UseExceptionHandler добавляет Middleware обработки исключений в не-Development средах.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Шаблон приложения Razor Pages предоставляет страницу ошибки (.cshtml) и класс PageModel (ErrorModel) в папке Pages. Для приложения MVC шаблон проекта содержит метод действия Error и представление ошибок для контроллера Home.

ПО промежуточного слоя обработки исключений повторно выполняет запрос, используя исходный метод HTTP. Если конечная точка обработчика ошибок ограничена определенным набором методов HTTP, она выполняется только для этих методов HTTP. Например, действие контроллера MVC, использующее атрибут [HttpGet], выполняется только для запросов GET. Чтобы гарантировать, что все запросы будут попадать на страницу пользовательской обработки ошибок, не ограничивайте их определённым набором HTTP-методов.

Для избирательного управления исключениями в зависимости от исходного метода HTTP:

  • Для Razor страниц создайте несколько методов-обработчиков. Например, используйте OnGet, чтобы обрабатывать исключения GET, и OnPost, чтобы обрабатывать исключения POST.
  • Для MVC примените атрибуты HTTP-команды к нескольким действиям. Например, используйте [HttpGet], чтобы обрабатывать исключения GET, и [HttpPost], чтобы обрабатывать исключения POST.

Чтобы разрешить пользователям, не прошедшим проверку подлинности, просматривать страницу пользовательской обработки ошибок, убедитесь, что она поддерживает анонимный доступ.

Откройте исключение

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

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string? ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();

        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "The file was not found.";
        }

        if (exceptionHandlerPathFeature?.Path == "/")
        {
            ExceptionMessage ??= string.Empty;
            ExceptionMessage += " Page: Home.";
        }
    }
}

Warning

Не передавайте клиентам конфиденциальную информацию об ошибках. Сохранение ошибок создает риски для безопасности.

Лямбда-обработчик исключений

Альтернативой пользовательской странице обработчика исключений является предоставление лямбда-функции для UseExceptionHandler. Использование лямбда-функции позволяет получить доступ к ошибке до возврата ответа.

В следующем коде для обработки исключений используется лямбда-выражение:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            // using static System.Net.Mime.MediaTypeNames;
            context.Response.ContentType = Text.Plain;

            await context.Response.WriteAsync("An exception was thrown.");

            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync(" The file was not found.");
            }

            if (exceptionHandlerPathFeature?.Path == "/")
            {
                await context.Response.WriteAsync(" Page: Home.");
            }
        });
    });

    app.UseHsts();
}

Warning

Не передавайте клиентам конфиденциальную информацию об ошибках. Сохранение ошибок создает риски для безопасности.

IExceptionHandler

IExceptionHandler — это интерфейс, который предоставляет разработчику обратный вызов для обработки известных исключений в едином месте.

IExceptionHandler реализации регистрируются через вызов IServiceCollection.AddExceptionHandler<T> [IServiceCollection.AddExceptionHandler<T>]. Время существования экземпляра IExceptionHandler — одноэлементное. Можно добавить несколько реализаций, и они вызываются в порядке регистрации.

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

В следующем примере показана реализация IExceptionHandler:

using Microsoft.AspNetCore.Diagnostics;

namespace ErrorHandlingSample
{
    public class CustomExceptionHandler : IExceptionHandler
    {
        private readonly ILogger<CustomExceptionHandler> logger;
        public CustomExceptionHandler(ILogger<CustomExceptionHandler> logger)
        {
            this.logger = logger;
        }
        public ValueTask<bool> TryHandleAsync(
            HttpContext httpContext,
            Exception exception,
            CancellationToken cancellationToken)
        {
            var exceptionMessage = exception.Message;
            logger.LogError(
                "Error Message: {exceptionMessage}, Time of occurrence {time}",
                exceptionMessage, DateTime.UtcNow);
            // Return false to continue with the default behavior
            // - or - return true to signal that this exception is handled
            return ValueTask.FromResult(false);
        }
    }
}

В следующем примере показано, как зарегистрировать реализацию IExceptionHandler для внедрения зависимостей:

using ErrorHandlingSample;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
builder.Services.AddExceptionHandler<CustomExceptionHandler>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// Remaining Program.cs code omitted for brevity

При выполнении предыдущего Development кода в среде:

  • Сначала вызывается CustomExceptionHandler для обработки исключения.
  • После регистрации исключения метод возвращает TryHandleException, поэтому отображается false.

В других средах:

  • Сначала вызывается CustomExceptionHandler для обработки исключения.
  • После регистрации исключения метод TryHandleException возвращает false, поэтому отображается страница /Error.

UseStatusCodePages

По умолчанию приложение ASP.NET Core не предоставляет страницу для кодов состояния ошибок HTTP, таких как код 404 Not Found (не найдено). Когда в приложении устанавливается код состояния ошибки HTTP 400–599 без текста, возвращается код состояния и пустой текст ответа. Чтобы включить обработчики по умолчанию, возвращающие только текст для распространенных кодов состояния ошибки, вызовите UseStatusCodePages в Program.cs:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages();

Вызовите UseStatusCodePages до ПО промежуточного слоя для обработки запросов. Например, вызовите UseStatusCodePages до ПО промежуточной обработки статических файлов и ПО промежуточной обработки конечных точек.

Если UseStatusCodePages не используется, при переходе по URL-адресу без конечной точки возвращается зависящее от браузера сообщение об ошибке, в котором указывается, что конечная точка не найдена. При вызове метода UseStatusCodePages браузер вернет следующий ответ:

Status Code: 404; Not Found

UseStatusCodePages обычно не применяется в рабочей среде, так как возвращает сообщение, бесполезное для пользователей.

Note

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

UseStatusCodePages со строкой формата

Чтобы настроить тип содержимого и текст ответа, используйте перегрузку UseStatusCodePages, которая принимает тип содержимого и строку форматирования.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");

В предыдущем коде {0} служит заполнителем для кода ошибки.

UseStatusCodePages со строкой формата обычно не применяется в рабочей среде, так как возвращает сообщение, бесполезное для пользователей.

UseStatusCodePages с использованием лямбда-выражения

Чтобы указать пользовательский код обработки ошибок и записи ответа, используйте перегрузку UseStatusCodePages, которая принимает лямбда-выражение.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages(async statusCodeContext =>
{
    // using static System.Net.Mime.MediaTypeNames;
    statusCodeContext.HttpContext.Response.ContentType = Text.Plain;

    await statusCodeContext.HttpContext.Response.WriteAsync(
        $"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});

UseStatusCodePages с функцией Lambda обычно не используется в рабочей среде, так как возвращает сообщение, не представляющее пользы для пользователей.

UseStatusCodePagesWithRedirects

Метод расширения UseStatusCodePagesWithRedirects:

  • Отправляет клиенту код состояния 302 — Found.
  • Перенаправляет клиент в конечную точку обработки ошибок, указанную в шаблоне URL-адреса. Конечная точка обработки ошибок обычно отображает сведения об ошибке и возвращает код HTTP 200.
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");

Шаблон URL-адреса может содержать заполнитель {0} для кода состояния, как показано в предыдущем коде. Если шаблон URL-адреса начинается с ~ (тильды), ~ заменяется на PathBase приложения. При указании конечной точки в приложении создайте представление MVC или страницу Razor для конечной точки.

Этот метод обычно используется, если приложение:

  • Должно перенаправлять клиент в другую конечную точку, что обычно бывает в случаях, когда другое приложение обрабатывает ошибку. Для веб-приложений в адресной строке браузера клиента отображается конечная точка перенаправления.
  • Не следует сохранять и возвращать исходный код состояния в ответе первоначального перенаправления.

UseStatusCodePagesWithReExecute

Метод расширения UseStatusCodePagesWithReExecute:

  • Позволяет создать текст ответа путем повторного выполнения конвейера запросов с использованием другого пути.
  • Не изменяет код состояния до или после повторного выполнения конвейера.

Новое выполнение конвейера может изменить код состояния ответа, так как новый конвейер имеет полный контроль над кодом состояния. Если новый конвейер не изменяет код состояния, исходный код состояния будет отправлен клиенту.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");

Если указывается конечная точка в приложении, создайте представление MVC или страницу Razor для конечной точки.

Этот метод обычно используется, если приложение:

  • Обрабатывает запрос без перенаправления к другой конечной точке. Для веб-приложений в адресной строке браузера клиента отображается изначально запрошенная конечная точка.
  • Сохраняет и возвращает исходный код состояния с ответом.

Шаблон URL-адреса должен начинаться с символа / и может содержать заполнитель {0} для кода состояния. Чтобы передать код состояния в качестве параметра строки запроса, передайте второй аргумент в UseStatusCodePagesWithReExecute. Рассмотрим пример.

var app = builder.Build();  
app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");

Конечная точка, которая обрабатывает ошибку, может получать исходный URL-адрес, вызвавший ошибку, как показано в следующем примере:

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
    public int OriginalStatusCode { get; set; }

    public string? OriginalPathAndQuery { get; set; }

    public void OnGet(int statusCode)
    {
        OriginalStatusCode = statusCode;

        var statusCodeReExecuteFeature =
            HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

        if (statusCodeReExecuteFeature is not null)
        {
            OriginalPathAndQuery = $"{statusCodeReExecuteFeature.OriginalPathBase}"
                                    + $"{statusCodeReExecuteFeature.OriginalPath}"
                                    + $"{statusCodeReExecuteFeature.OriginalQueryString}";

        }
    }
}

Так как это ПО промежуточного слоя может повторно выполнить конвейер запросов:

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

Отключение страниц с кодами состояния

Чтобы отключить страницы кодов состояния для метода контроллера или действия MVC, используйте атрибут [SkipStatusCodePages].

Чтобы отключить страницы кодов состояния для конкретных запросов в методе обработчика Razor Pages или в контроллере MVC, используйте IStatusCodePagesFeature.

public void OnGet()
{
    var statusCodePagesFeature =
        HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature is not null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}

Код обработки исключений

Код на страницах обработки исключений также может создавать исключения. Рабочие страницы ошибок необходимо тщательно тестировать, чтобы они не создавали собственных исключений.

Заголовки ответа

После отправки заголовков для ответа происходит следующее:

  • Приложение не может изменить код состояния ответа.
  • Нельзя запустить страницу или обработчик исключений. Необходимо завершить ответ или прервать подключение.

Обработка исключений на сервере

Помимо логики обработки исключений в приложении, реализация HTTP-сервера также может обрабатывать некоторые исключения. Если сервер перехватывает исключение перед отправкой заголовков ответа, сервер отправляет 500 - Internal Server Error ответ без текста ответа. Если сервер перехватывает исключение после отправки заголовков ответа, он закрывает соединение. Запросы, не обработанные приложением, обрабатываются сервером. Все исключения, возникшие при обработке запроса серверов, обрабатываются с помощью механизма обработки исключений на сервере. Пользовательские страницы ошибок приложения, компоненты промежуточного ПО для обработки исключений и фильтры не влияют на это поведение.

Обработка исключений при запуске

Исключения, возникающие во время запуска приложения, могут обрабатываться только на уровне размещения. Узел можно настроить для перехвата ошибок при загрузке и записи подробных сведений об ошибках.

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

  • Уровень хостинга регистрирует в журнале критическое исключение.
  • Процесс dotnet аварийно завершается.
  • Если приложение запущено на HTTP-сервере Kestrel, страница со сведениями об ошибке не отображается.

При работе в службах IIS (или Службе приложений Azure) либо IIS Expressмодуль ASP.NET Core возвращает ошибку 502.5 Process Failure (ошибка процесса), если процесс невозможно запустить. Дополнительные сведения см. в статье Устранение неполадок с ASP.NET Core в Службе приложений Azure и IIS.

Страница ошибок базы данных

Фильтр исключений для страницы разработчика базы данных AddDatabaseDeveloperPageExceptionFilter перехватывает исключения, относящиеся к базе данных, которые могут быть устранены с помощью миграций Entity Framework Core. При возникновении этих исключений формируется HTML-ответ с подробными сведениями о возможных действиях для устранения проблемы. Эта страница включена только в Development среде. В следующем коде добавляется фильтр исключений для страницы разработчика базы данных:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();

Фильтры исключений

В приложениях MVC фильтры исключений можно настраивать как глобально, так и для отдельных контроллеров или действий. В приложениях Razor Pages они могут быть настроены глобально или для модели страницы. Эти фильтры обрабатывают все необработанные исключения, которые возникают во время выполнения действия контроллера или другого фильтра. Дополнительные сведения см. в статье Фильтры в ASP.NET Core.

Фильтры исключений полезны при перехвате исключений, которые возникают в действиях MVC. Однако эти фильтры не так гибки, как встроенное ПО промежуточного слоя для обработки исключенийUseExceptionHandler. Мы рекомендуем использовать UseExceptionHandler, если ошибки не нужно обрабатывать по-разному в зависимости от выбранного действия MVC.

Ошибки состояния модели

Сведения о том, как обрабатывать ошибки состояния модели, см. в статьях о привязке модели и проверке модели.

Сведения о проблеме

Сведения о проблеме — это не единственный формат ответа, описывающий ошибку API HTTP, однако они часто используются для сообщения об ошибках для API HTTP.

Служба сведений о проблеме IProblemDetailsService реализует интерфейс, который поддерживает создание сведений о проблеме в ASP.NET Core. Метод расширения AddProblemDetails(IServiceCollection) для IServiceCollection регистрирует реализацию IProblemDetailsService по умолчанию.

В приложениях ASP.NET Core следующее ПО промежуточной обработки создает HTTP-ответы со сведениями о проблеме при вызове AddProblemDetails, за исключением случаев, когда HTTP-заголовок запроса Accept не включает один из типов содержимого, поддерживаемых зарегистрированным IProblemDetailsWriter (по умолчанию: application/json):

  • ExceptionHandlerMiddleware: создает ответ с подробными сведениями о проблеме, если пользовательский обработчик не задан.
  • StatusCodePagesMiddleware: Создает ответ с подробными сведениями о проблеме по умолчанию.
  • DeveloperExceptionPageMiddleware: создает ответ с подробными сведениями о проблеме в среде разработки, если заголовок HTTP запроса Accept не содержит text/html.

Следующий код настраивает приложение на создание ответа с подробными сведениями о проблеме для всех ответов HTTP об ошибках клиента и сервера, которые ещё не имеют содержимого в теле:

builder.Services.AddProblemDetails();

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();

В следующем разделе показано, как настроить тело ответа с подробными сведениями о проблеме.

Настройка сведений о проблеме

Автоматическое создание объекта ProblemDetails можно настроить с помощью любого из следующих параметров:

  1. Используйте ProblemDetailsOptions.CustomizeProblemDetails
  2. Использовать пользовательский IProblemDetailsWriter
  3. Вызовите IProblemDetailsService в промежуточном ПО

CustomizeProblemDetails операция

Сведения о созданной проблеме можно настроить с помощью CustomizeProblemDetails, и настройки применяются ко всем автоматически созданным сведениям о проблеме.

В следующем коде для задания ProblemDetailsOptions используется CustomizeProblemDetails:

builder.Services.AddProblemDetails(options =>
    options.CustomizeProblemDetails = ctx =>
            ctx.ProblemDetails.Extensions.Add("nodeId", Environment.MachineName));

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();

Например, результат конечной точки HTTP Status 400 Bad Request приводит к следующему телу ответа со сведениями о проблеме:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "nodeId": "my-machine-name"
}

Настраиваемый код IProblemDetailsWriter

Для расширенной настройки можно создать реализацию IProblemDetailsWriter.

public class SampleProblemDetailsWriter : IProblemDetailsWriter
{
    // Indicates that only responses with StatusCode == 400
    // are handled by this writer. All others are
    // handled by different registered writers if available.
    public bool CanWrite(ProblemDetailsContext context)
        => context.HttpContext.Response.StatusCode == 400;

    public ValueTask WriteAsync(ProblemDetailsContext context)
    {
        // Additional customizations.

        // Write to the response.
        var response = context.HttpContext.Response;
        return new ValueTask(response.WriteAsJsonAsync(context.ProblemDetails));
    }
}

Примечание: При использовании пользовательского IProblemDetailsWriter пользовательский IProblemDetailsWriter должен быть зарегистрирован перед вызовом AddRazorPages, AddControllers, AddControllersWithViews или AddMvc:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IProblemDetailsWriter, SampleProblemDetailsWriter>();

var app = builder.Build();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsWriter>() is
            { } problemDetailsService)
        {

            if (problemDetailsService.CanWrite(new ProblemDetailsContext() { HttpContext = context }))
            {
                (string Detail, string Type) details = mathErrorFeature.MathError switch
                {
                    MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                        "https://en.wikipedia.org/wiki/Division_by_zero"),
                    _ => ("Negative or complex numbers are not valid input.",
                        "https://en.wikipedia.org/wiki/Square_root")
                };

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                    {
                        Title = "Bad Input",
                        Detail = details.Detail,
                        Type = details.Type
                    }
                });
            }
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.DivisionByZeroError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.NegativeRadicandError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.Run();

Подробности проблемы в промежуточном ПО

Альтернативный подход к использованию ProblemDetailsOptions с CustomizeProblemDetails заключается в настройке ProblemDetails в промежуточном ПО. Ответ с подробными сведениями о проблеме можно сформировать, вызвав IProblemDetailsService.WriteAsync:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStatusCodePages();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsService>() is
                                                           { } problemDetailsService)
        {
            (string Detail, string Type) details = mathErrorFeature.MathError switch
            {
                MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                "https://en.wikipedia.org/wiki/Division_by_zero"),
                _ => ("Negative or complex numbers are not valid input.", 
                "https://en.wikipedia.org/wiki/Square_root")
            };

            await problemDetailsService.WriteAsync(new ProblemDetailsContext
            {
                HttpContext = context,
                ProblemDetails =
                {
                    Title = "Bad Input",
                    Detail = details.Detail,
                    Type = details.Type
                }
            });
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.DivisionByZeroError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.NegativeRadicandError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.MapControllers();

app.Run();

В приведенном выше коде конечные точки минимального API /divide и /squareroot возвращают ожидаемый кастомизированный ответ на проблему при ошибочном вводе.

Эндпоинты контроллера API при некорректных входных данных возвращают ответ с описанием ошибки по умолчанию, а не пользовательский ответ с описанием ошибки. Стандартный ответ о проблеме возвращается, поскольку контроллер API записал в поток ответа сведения о проблеме для кодов состояния ошибок до вызова IProblemDetailsService.WriteAsync, и затем ответ не записывается повторно.

Следующий код ValuesController возвращает BadRequestResult, который записывает данные в поток ответа и поэтому не позволяет вернуть пользовательский ответ о проблеме.

[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // /api/values/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }

}

Следующий Values3Controller возвращает ControllerBase.Problem, поэтому возвращается ожидаемый пользовательский результат ошибки:

[Route("api/[controller]/[action]")]
[ApiController]
public class Values3Controller : ControllerBase
{
    // /api/values3/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Divison by zero is not defined.",
                type: "https://en.wikipedia.org/wiki/Division_by_zero",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values3/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Negative or complex numbers are not valid input.",
                type: "https://en.wikipedia.org/wiki/Square_root",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Math.Sqrt(radicand));
    }

}

Создать полезную нагрузку ProblemDetails для исключений

Рассмотрим следующее приложение:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapControllers();
app.Run();

В средах, отличных от разработки, при возникновении исключения ниже приведен стандартный ответ ProblemDetails, возвращаемый клиенту:

{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500,"traceId":"00-b644<snip>-00"
}

Для большинства приложений предыдущий код — это все, что необходимо для исключений. Однако в следующем разделе показано, как получить более подробные ответы на проблемы.

Альтернативой пользовательской странице обработчика исключений является предоставление лямбда-функции для UseExceptionHandler. Использование лямбда-выражения позволяет получить доступ к ошибке и записать в ответ сведения о проблеме с помощью IProblemDetailsService.WriteAsync:

using Microsoft.AspNetCore.Diagnostics;
using static System.Net.Mime.MediaTypeNames;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = Text.Plain;

            var title = "Bad Input";
            var detail = "Invalid input";
            var type = "https://errors.example.com/badInput";

            if (context.RequestServices.GetService<IProblemDetailsService>() is
                { } problemDetailsService)
            {
                var exceptionHandlerFeature =
               context.Features.Get<IExceptionHandlerFeature>();

                var exceptionType = exceptionHandlerFeature?.Error;
                if (exceptionType != null &&
                   exceptionType.Message.Contains("infinity"))
                {
                    title = "Argument exception";
                    detail = "Invalid input";
                    type = "https://errors.example.com/argumentException";
                }

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                {
                    Title = title,
                    Detail = detail,
                    Type = type
                }
                });
            }
        });
    });
}

app.MapControllers();
app.Run();

Warning

Не передавайте клиентам конфиденциальную информацию об ошибках. Сохранение ошибок создает риски для безопасности.

Альтернативный подход к созданию сведений о проблеме — использовать сторонний пакет NuGet Hellang.Middleware.ProblemDetails , который можно использовать для сопоставления исключений и ошибок клиента с сведениями о проблеме.

Дополнительные ресурсы

Автор: Том Дикстра (Tom Dykstra)

В этой статье рассматриваются основные методы обработки ошибок в веб-приложениях ASP.NET Core. См. также Обработка ошибок в API ASP.NET Core.

Страница исключения для разработчиков

Страница исключений для разработчика содержит подробные сведения о необработанных исключениях запросов. Приложения ASP.NET Core по умолчанию включают страницу исключений для разработчиков, если одновременно выполняются оба условия:

  • Запуск в Development среде.
  • Приложение создано с использованием текущих шаблонов, то есть с помощью WebApplication.CreateBuilder. Приложения, созданные с помощью WebHost.CreateDefaultBuilder, должны поддерживать страницу исключений разработчика путем вызова app.UseDeveloperExceptionPage и Configure.

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

Подробные сведения об исключении не должны отображаться публично при запуске приложения в Production среде. Дополнительные сведения о настройке сред см. в ASP.NET средах выполнения Core.

Страница исключений для разработчика может содержать следующие сведения об исключении и запросе:

  • Трассировка стека
  • параметры строки запроса (при наличии);
  • Cookies, если таковой есть
  • Headers

Страница исключений для разработчика не обязательно содержит какую-либо информацию. Используйте Ведение журнала для получения полных сведений об ошибке.

Страница обработчика исключений

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

  • Перехватывает и записывает в журнал необработанные исключения.
  • повторно выполняет запрос в альтернативном конвейере по указанному пути. Запрос не выполняется повторно, если запущен отклик. Созданный шаблоном код повторно выполняет запрос, используя путь /Error.

Warning

Если альтернативный конвейер вызывает собственное исключение, middleware обработки исключений повторно выбрасывает исходное исключение.

Так как это ПО промежуточного слоя может повторно выполнить конвейер запросов:

  • Промежуточное ПО должно поддерживать реентерабельность при повторном входе с тем же запросом. Обычно это означает либо очищать их состояние после вызова _next, либо кэшировать результаты их обработки в HttpContext, чтобы не выполнять её повторно. При работе с текстом запроса это означает буферизацию или кэширование результатов, таких как средство чтения форм.
  • Для перегрузки UseExceptionHandler(IApplicationBuilder, String) , используемой в шаблонах, изменяется только путь запроса, а данные маршрута очищаются. Данные запроса, такие как заголовки, метод и элементы, повторно используются без изменений.
  • Службы с ограниченной областью действия остаются неизменными.

В следующем примере UseExceptionHandler добавляет Middleware обработки исключений в не-Development средах.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Шаблон приложения Razor Pages предоставляет страницу ошибки (.cshtml) и класс PageModel (ErrorModel) в папке Pages. Для приложения MVC шаблон проекта содержит метод действия Error и представление ошибок для контроллера Home.

ПО промежуточного слоя обработки исключений повторно выполняет запрос, используя исходный метод HTTP. Если конечная точка обработчика ошибок ограничена определенным набором методов HTTP, она выполняется только для этих методов HTTP. Например, действие контроллера MVC, использующее атрибут [HttpGet], выполняется только для запросов GET. Чтобы гарантировать, что все запросы будут попадать на страницу пользовательской обработки ошибок, не ограничивайте их определённым набором HTTP-методов.

Для избирательного управления исключениями в зависимости от исходного метода HTTP:

  • Для Razor страниц создайте несколько методов-обработчиков. Например, используйте OnGet, чтобы обрабатывать исключения GET, и OnPost, чтобы обрабатывать исключения POST.
  • Для MVC примените атрибуты HTTP-команды к нескольким действиям. Например, используйте [HttpGet], чтобы обрабатывать исключения GET, и [HttpPost], чтобы обрабатывать исключения POST.

Чтобы разрешить пользователям, не прошедшим проверку подлинности, просматривать страницу пользовательской обработки ошибок, убедитесь, что она поддерживает анонимный доступ.

Откройте исключение

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

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string? ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();

        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "The file was not found.";
        }

        if (exceptionHandlerPathFeature?.Path == "/")
        {
            ExceptionMessage ??= string.Empty;
            ExceptionMessage += " Page: Home.";
        }
    }
}

Warning

Не передавайте клиентам конфиденциальную информацию об ошибках. Сохранение ошибок создает риски для безопасности.

Лямбда-обработчик исключений

Альтернативой пользовательской странице обработчика исключений является предоставление лямбда-функции для UseExceptionHandler. Использование лямбда-функции позволяет получить доступ к ошибке до возврата ответа.

В следующем коде для обработки исключений используется лямбда-выражение:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            // using static System.Net.Mime.MediaTypeNames;
            context.Response.ContentType = Text.Plain;

            await context.Response.WriteAsync("An exception was thrown.");

            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync(" The file was not found.");
            }

            if (exceptionHandlerPathFeature?.Path == "/")
            {
                await context.Response.WriteAsync(" Page: Home.");
            }
        });
    });

    app.UseHsts();
}

Warning

Не передавайте клиентам конфиденциальную информацию об ошибках. Сохранение ошибок создает риски для безопасности.

UseStatusCodePages

По умолчанию приложение ASP.NET Core не предоставляет страницу для кодов состояния ошибок HTTP, таких как код 404 Not Found (не найдено). Когда в приложении устанавливается код состояния ошибки HTTP 400–599 без текста, возвращается код состояния и пустой текст ответа. Чтобы включить обработчики по умолчанию, возвращающие только текст для распространенных кодов состояния ошибки, вызовите UseStatusCodePages в Program.cs:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages();

Вызовите UseStatusCodePages до ПО промежуточного слоя для обработки запросов. Например, вызовите UseStatusCodePages до ПО промежуточной обработки статических файлов и ПО промежуточной обработки конечных точек.

Если UseStatusCodePages не используется, при переходе по URL-адресу без конечной точки возвращается зависящее от браузера сообщение об ошибке, в котором указывается, что конечная точка не найдена. При вызове метода UseStatusCodePages браузер вернет следующий ответ:

Status Code: 404; Not Found

UseStatusCodePages обычно не применяется в рабочей среде, так как возвращает сообщение, бесполезное для пользователей.

Note

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

UseStatusCodePages со строкой формата

Чтобы настроить тип содержимого и текст ответа, используйте перегрузку UseStatusCodePages, которая принимает тип содержимого и строку форматирования.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");

В предыдущем коде {0} служит заполнителем для кода ошибки.

UseStatusCodePages со строкой формата обычно не применяется в рабочей среде, так как возвращает сообщение, бесполезное для пользователей.

UseStatusCodePages с использованием лямбда-выражения

Чтобы указать пользовательский код обработки ошибок и записи ответа, используйте перегрузку UseStatusCodePages, которая принимает лямбда-выражение.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages(async statusCodeContext =>
{
    // using static System.Net.Mime.MediaTypeNames;
    statusCodeContext.HttpContext.Response.ContentType = Text.Plain;

    await statusCodeContext.HttpContext.Response.WriteAsync(
        $"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});

UseStatusCodePages с функцией Lambda обычно не используется в рабочей среде, так как возвращает сообщение, не представляющее пользы для пользователей.

UseStatusCodePagesWithRedirects

Метод расширения UseStatusCodePagesWithRedirects:

  • Отправляет клиенту код состояния 302 — Found.
  • Перенаправляет клиент в конечную точку обработки ошибок, указанную в шаблоне URL-адреса. Конечная точка обработки ошибок обычно отображает сведения об ошибке и возвращает код HTTP 200.
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");

Шаблон URL-адреса может содержать заполнитель {0} для кода состояния, как показано в предыдущем коде. Если шаблон URL-адреса начинается с ~ (тильды), ~ заменяется на PathBase приложения. При указании конечной точки в приложении создайте представление MVC или страницу Razor для конечной точки.

Этот метод обычно используется, если приложение:

  • Должно перенаправлять клиент в другую конечную точку, что обычно бывает в случаях, когда другое приложение обрабатывает ошибку. Для веб-приложений в адресной строке браузера клиента отображается конечная точка перенаправления.
  • Не следует сохранять и возвращать исходный код состояния в ответе первоначального перенаправления.

UseStatusCodePagesWithReExecute

Метод расширения UseStatusCodePagesWithReExecute:

  • Позволяет создать текст ответа путем повторного выполнения конвейера запросов с использованием другого пути.
  • Не изменяет код состояния до или после повторного выполнения конвейера.

Новое выполнение конвейера может изменить код состояния ответа, так как новый конвейер имеет полный контроль над кодом состояния. Если новый конвейер не изменяет код состояния, исходный код состояния будет отправлен клиенту.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");

Если указывается конечная точка в приложении, создайте представление MVC или страницу Razor для конечной точки.

Этот метод обычно используется, если приложение:

  • Обрабатывает запрос без перенаправления к другой конечной точке. Для веб-приложений в адресной строке браузера клиента отображается изначально запрошенная конечная точка.
  • Сохраняет и возвращает исходный код состояния с ответом.

Шаблон URL-адреса должен начинаться с символа / и может содержать заполнитель {0} для кода состояния. Чтобы передать код состояния в качестве параметра строки запроса, передайте второй аргумент в UseStatusCodePagesWithReExecute. Рассмотрим пример.

var app = builder.Build();  
app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");

Конечная точка, которая обрабатывает ошибку, может получать исходный URL-адрес, вызвавший ошибку, как показано в следующем примере:

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
    public int OriginalStatusCode { get; set; }

    public string? OriginalPathAndQuery { get; set; }

    public void OnGet(int statusCode)
    {
        OriginalStatusCode = statusCode;

        var statusCodeReExecuteFeature =
            HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

        if (statusCodeReExecuteFeature is not null)
        {
            OriginalPathAndQuery = $"{statusCodeReExecuteFeature.OriginalPathBase}"
                                    + $"{statusCodeReExecuteFeature.OriginalPath}"
                                    + $"{statusCodeReExecuteFeature.OriginalQueryString}";

        }
    }
}

Так как это ПО промежуточного слоя может повторно выполнить конвейер запросов:

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

Отключение страниц с кодами состояния

Чтобы отключить страницы кодов состояния для метода контроллера или действия MVC, используйте атрибут [SkipStatusCodePages].

Чтобы отключить страницы кодов состояния для конкретных запросов в методе обработчика Razor Pages или в контроллере MVC, используйте IStatusCodePagesFeature.

public void OnGet()
{
    var statusCodePagesFeature =
        HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature is not null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}

Код обработки исключений

Код на страницах обработки исключений также может создавать исключения. Рабочие страницы ошибок необходимо тщательно тестировать, чтобы они не создавали собственных исключений.

Заголовки ответа

После отправки заголовков для ответа происходит следующее:

  • Приложение не может изменить код состояния ответа.
  • Нельзя запустить страницу или обработчик исключений. Необходимо завершить ответ или прервать подключение.

Обработка исключений на сервере

Помимо логики обработки исключений в приложении, реализация HTTP-сервера также может обрабатывать некоторые исключения. Если сервер перехватывает исключение перед отправкой заголовков ответа, сервер отправляет 500 - Internal Server Error ответ без текста ответа. Если сервер перехватывает исключение после отправки заголовков ответа, он закрывает соединение. Запросы, не обработанные приложением, обрабатываются сервером. Все исключения, возникшие при обработке запроса серверов, обрабатываются с помощью механизма обработки исключений на сервере. Пользовательские страницы ошибок приложения, компоненты промежуточного ПО для обработки исключений и фильтры не влияют на это поведение.

Обработка исключений при запуске

Исключения, возникающие во время запуска приложения, могут обрабатываться только на уровне размещения. Узел можно настроить для перехвата ошибок при загрузке и записи подробных сведений об ошибках.

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

  • Уровень хостинга регистрирует в журнале критическое исключение.
  • Процесс dotnet аварийно завершается.
  • Если приложение запущено на HTTP-сервере Kestrel, страница со сведениями об ошибке не отображается.

При работе в службах IIS (или Службе приложений Azure) либо IIS Expressмодуль ASP.NET Core возвращает ошибку 502.5 Process Failure (ошибка процесса), если процесс невозможно запустить. Дополнительные сведения см. в статье Устранение неполадок с ASP.NET Core в Службе приложений Azure и IIS.

Страница ошибок базы данных

Фильтр исключений для страницы разработчика базы данных AddDatabaseDeveloperPageExceptionFilter перехватывает исключения, относящиеся к базе данных, которые могут быть устранены с помощью миграций Entity Framework Core. При возникновении этих исключений формируется HTML-ответ с подробными сведениями о возможных действиях для устранения проблемы. Эта страница включена только в Development среде. В следующем коде добавляется фильтр исключений для страницы разработчика базы данных:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();

Фильтры исключений

В приложениях MVC фильтры исключений можно настраивать как глобально, так и для отдельных контроллеров или действий. В приложениях Razor Pages они могут быть настроены глобально или для модели страницы. Эти фильтры обрабатывают все необработанные исключения, которые возникают во время выполнения действия контроллера или другого фильтра. Дополнительные сведения см. в статье Фильтры в ASP.NET Core.

Фильтры исключений полезны при перехвате исключений, которые возникают в действиях MVC. Однако эти фильтры не так гибки, как встроенное ПО промежуточного слоя для обработки исключенийUseExceptionHandler. Мы рекомендуем использовать UseExceptionHandler, если ошибки не нужно обрабатывать по-разному в зависимости от выбранного действия MVC.

Ошибки состояния модели

Сведения о том, как обрабатывать ошибки состояния модели, см. в статьях о привязке модели и проверке модели.

Сведения о проблеме

Сведения о проблеме — это не единственный формат ответа, описывающий ошибку API HTTP, однако они часто используются для сообщения об ошибках для API HTTP.

Служба сведений о проблеме IProblemDetailsService реализует интерфейс, который поддерживает создание сведений о проблеме в ASP.NET Core. Метод расширения AddProblemDetails(IServiceCollection) для IServiceCollection регистрирует реализацию IProblemDetailsService по умолчанию.

В приложениях ASP.NET Core следующее ПО промежуточной обработки создает HTTP-ответы со сведениями о проблеме при вызове AddProblemDetails, за исключением случаев, когда HTTP-заголовок запроса Accept не включает один из типов содержимого, поддерживаемых зарегистрированным IProblemDetailsWriter (по умолчанию: application/json):

  • ExceptionHandlerMiddleware: создает ответ с подробными сведениями о проблеме, если пользовательский обработчик не задан.
  • StatusCodePagesMiddleware: Создает ответ с подробными сведениями о проблеме по умолчанию.
  • DeveloperExceptionPageMiddleware: создает ответ с подробными сведениями о проблеме в среде разработки, если заголовок HTTP запроса Accept не содержит text/html.

Следующий код настраивает приложение на создание ответа с подробными сведениями о проблеме для всех ответов HTTP об ошибках клиента и сервера, которые ещё не имеют содержимого в теле:

builder.Services.AddProblemDetails();

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();

В следующем разделе показано, как настроить тело ответа с подробными сведениями о проблеме.

Настройка сведений о проблеме

Автоматическое создание объекта ProblemDetails можно настроить с помощью любого из следующих параметров:

  1. Используйте ProblemDetailsOptions.CustomizeProblemDetails
  2. Использовать пользовательский IProblemDetailsWriter
  3. Вызовите IProblemDetailsService в промежуточном ПО

CustomizeProblemDetails операция

Сведения о созданной проблеме можно настроить с помощью CustomizeProblemDetails, и настройки применяются ко всем автоматически созданным сведениям о проблеме.

В следующем коде для задания ProblemDetailsOptions используется CustomizeProblemDetails:

builder.Services.AddProblemDetails(options =>
    options.CustomizeProblemDetails = ctx =>
            ctx.ProblemDetails.Extensions.Add("nodeId", Environment.MachineName));

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();

Например, результат конечной точки HTTP Status 400 Bad Request приводит к следующему телу ответа со сведениями о проблеме:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "nodeId": "my-machine-name"
}

Настраиваемый код IProblemDetailsWriter

Для расширенной настройки можно создать реализацию IProblemDetailsWriter.

public class SampleProblemDetailsWriter : IProblemDetailsWriter
{
    // Indicates that only responses with StatusCode == 400
    // are handled by this writer. All others are
    // handled by different registered writers if available.
    public bool CanWrite(ProblemDetailsContext context)
        => context.HttpContext.Response.StatusCode == 400;

    public ValueTask WriteAsync(ProblemDetailsContext context)
    {
        // Additional customizations.

        // Write to the response.
        var response = context.HttpContext.Response;
        return new ValueTask(response.WriteAsJsonAsync(context.ProblemDetails));
    }
}

Примечание: При использовании пользовательского IProblemDetailsWriter пользовательский IProblemDetailsWriter должен быть зарегистрирован перед вызовом AddRazorPages, AddControllers, AddControllersWithViews или AddMvc:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IProblemDetailsWriter, SampleProblemDetailsWriter>();

var app = builder.Build();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsWriter>() is
            { } problemDetailsService)
        {

            if (problemDetailsService.CanWrite(new ProblemDetailsContext() { HttpContext = context }))
            {
                (string Detail, string Type) details = mathErrorFeature.MathError switch
                {
                    MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                        "https://en.wikipedia.org/wiki/Division_by_zero"),
                    _ => ("Negative or complex numbers are not valid input.",
                        "https://en.wikipedia.org/wiki/Square_root")
                };

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                    {
                        Title = "Bad Input",
                        Detail = details.Detail,
                        Type = details.Type
                    }
                });
            }
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.DivisionByZeroError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.NegativeRadicandError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.Run();

Подробности проблемы в промежуточном ПО

Альтернативный подход к использованию ProblemDetailsOptions с CustomizeProblemDetails заключается в настройке ProblemDetails в промежуточном ПО. Ответ с подробными сведениями о проблеме можно сформировать, вызвав IProblemDetailsService.WriteAsync:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStatusCodePages();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsService>() is
                                                           { } problemDetailsService)
        {
            (string Detail, string Type) details = mathErrorFeature.MathError switch
            {
                MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                "https://en.wikipedia.org/wiki/Division_by_zero"),
                _ => ("Negative or complex numbers are not valid input.", 
                "https://en.wikipedia.org/wiki/Square_root")
            };

            await problemDetailsService.WriteAsync(new ProblemDetailsContext
            {
                HttpContext = context,
                ProblemDetails =
                {
                    Title = "Bad Input",
                    Detail = details.Detail,
                    Type = details.Type
                }
            });
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.DivisionByZeroError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.NegativeRadicandError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.MapControllers();

app.Run();

В приведенном выше коде конечные точки минимального API /divide и /squareroot возвращают ожидаемый кастомизированный ответ на проблему при ошибочном вводе.

Эндпоинты контроллера API при некорректных входных данных возвращают ответ с описанием ошибки по умолчанию, а не пользовательский ответ с описанием ошибки. Стандартный ответ о проблеме возвращается, поскольку контроллер API записал в поток ответа сведения о проблеме для кодов состояния ошибок до вызова IProblemDetailsService.WriteAsync, и затем ответ не записывается повторно.

Следующий код ValuesController возвращает BadRequestResult, который записывает данные в поток ответа и поэтому не позволяет вернуть пользовательский ответ о проблеме.

[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // /api/values/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }

}

Следующий Values3Controller возвращает ControllerBase.Problem, поэтому возвращается ожидаемый пользовательский результат ошибки:

[Route("api/[controller]/[action]")]
[ApiController]
public class Values3Controller : ControllerBase
{
    // /api/values3/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Divison by zero is not defined.",
                type: "https://en.wikipedia.org/wiki/Division_by_zero",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values3/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Negative or complex numbers are not valid input.",
                type: "https://en.wikipedia.org/wiki/Square_root",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Math.Sqrt(radicand));
    }

}

Создать полезную нагрузку ProblemDetails для исключений

Рассмотрим следующее приложение:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapControllers();
app.Run();

В средах, отличных от разработки, при возникновении исключения ниже приведен стандартный ответ ProblemDetails, возвращаемый клиенту:

{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500,"traceId":"00-b644<snip>-00"
}

Для большинства приложений предыдущий код — это все, что необходимо для исключений. Однако в следующем разделе показано, как получить более подробные ответы на проблемы.

Альтернативой пользовательской странице обработчика исключений является предоставление лямбда-функции для UseExceptionHandler. Использование лямбда-выражения позволяет получить доступ к ошибке и записать в ответ сведения о проблеме с помощью IProblemDetailsService.WriteAsync:

using Microsoft.AspNetCore.Diagnostics;
using static System.Net.Mime.MediaTypeNames;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = Text.Plain;

            var title = "Bad Input";
            var detail = "Invalid input";
            var type = "https://errors.example.com/badInput";

            if (context.RequestServices.GetService<IProblemDetailsService>() is
                { } problemDetailsService)
            {
                var exceptionHandlerFeature =
               context.Features.Get<IExceptionHandlerFeature>();

                var exceptionType = exceptionHandlerFeature?.Error;
                if (exceptionType != null &&
                   exceptionType.Message.Contains("infinity"))
                {
                    title = "Argument exception";
                    detail = "Invalid input";
                    type = "https://errors.example.com/argumentException";
                }

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                {
                    Title = title,
                    Detail = detail,
                    Type = type
                }
                });
            }
        });
    });
}

app.MapControllers();
app.Run();

Warning

Не передавайте клиентам конфиденциальную информацию об ошибках. Сохранение ошибок создает риски для безопасности.

Альтернативный подход к созданию сведений о проблеме — использовать сторонний пакет NuGet Hellang.Middleware.ProblemDetails , который можно использовать для сопоставления исключений и ошибок клиента с сведениями о проблеме.

Дополнительные ресурсы

Автор: Том Дикстра (Tom Dykstra)

В этой статье рассматриваются основные методы обработки ошибок в веб-приложениях ASP.NET Core. См. Обработка ошибок в API ASP.NET Core для веб-API.

Страница исключения для разработчиков

Страница исключений для разработчика содержит подробные сведения о необработанных исключениях запросов. Приложения ASP.NET Core по умолчанию включают страницу исключений для разработчиков, если одновременно выполняются оба условия:

  • Запуск в Development среде.
  • Приложение создано с использованием текущих шаблонов, то есть с помощью WebApplication.CreateBuilder. Приложения, созданные с помощью WebHost.CreateDefaultBuilder, должны поддерживать страницу исключений разработчика путем вызова app.UseDeveloperExceptionPage и Configure.

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

Подробные сведения об исключении не должны отображаться публично при запуске приложения в Production среде. Дополнительные сведения о настройке сред см. в ASP.NET средах выполнения Core.

Страница исключений для разработчика может содержать следующие сведения об исключении и запросе:

  • Трассировка стека
  • параметры строки запроса (при наличии);
  • Cookies, если таковой есть
  • Headers

Страница исключений для разработчика не обязательно содержит какую-либо информацию. Используйте Ведение журнала для получения полных сведений об ошибке.

Страница обработчика исключений

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

  • Перехватывает и записывает в журнал необработанные исключения.
  • повторно выполняет запрос в альтернативном конвейере по указанному пути. Запрос не выполняется повторно, если запущен отклик. Созданный шаблоном код повторно выполняет запрос, используя путь /Error.

Warning

Если альтернативный конвейер вызывает собственное исключение, middleware обработки исключений повторно выбрасывает исходное исключение.

В следующем примере UseExceptionHandler добавляет Middleware обработки исключений в не-Development средах.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Шаблон приложения Razor Pages предоставляет страницу ошибки (.cshtml) и класс PageModel (ErrorModel) в папке Pages. Для приложения MVC шаблон проекта содержит метод действия Error и представление ошибок для контроллера Home.

ПО промежуточного слоя обработки исключений повторно выполняет запрос, используя исходный метод HTTP. Если конечная точка обработчика ошибок ограничена определенным набором методов HTTP, она выполняется только для этих методов HTTP. Например, действие контроллера MVC, использующее атрибут [HttpGet], выполняется только для запросов GET. Чтобы гарантировать, что все запросы будут попадать на страницу пользовательской обработки ошибок, не ограничивайте их определённым набором HTTP-методов.

Для избирательного управления исключениями в зависимости от исходного метода HTTP:

  • Для Razor страниц создайте несколько методов-обработчиков. Например, используйте OnGet, чтобы обрабатывать исключения GET, и OnPost, чтобы обрабатывать исключения POST.
  • Для MVC примените атрибуты HTTP-команды к нескольким действиям. Например, используйте [HttpGet], чтобы обрабатывать исключения GET, и [HttpPost], чтобы обрабатывать исключения POST.

Чтобы разрешить пользователям, не прошедшим проверку подлинности, просматривать страницу пользовательской обработки ошибок, убедитесь, что она поддерживает анонимный доступ.

Откройте исключение

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

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string? ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();

        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "The file was not found.";
        }

        if (exceptionHandlerPathFeature?.Path == "/")
        {
            ExceptionMessage ??= string.Empty;
            ExceptionMessage += " Page: Home.";
        }
    }
}

Warning

Не передавайте клиентам конфиденциальную информацию об ошибках. Сохранение ошибок создает риски для безопасности.

Лямбда-обработчик исключений

Альтернативой пользовательской странице обработчика исключений является предоставление лямбда-функции для UseExceptionHandler. Использование лямбда-функции позволяет получить доступ к ошибке до возврата ответа.

В следующем коде для обработки исключений используется лямбда-выражение:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            // using static System.Net.Mime.MediaTypeNames;
            context.Response.ContentType = Text.Plain;

            await context.Response.WriteAsync("An exception was thrown.");

            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync(" The file was not found.");
            }

            if (exceptionHandlerPathFeature?.Path == "/")
            {
                await context.Response.WriteAsync(" Page: Home.");
            }
        });
    });

    app.UseHsts();
}

Warning

Не передавайте клиентам конфиденциальную информацию об ошибках. Сохранение ошибок создает риски для безопасности.

UseStatusCodePages

По умолчанию приложение ASP.NET Core не предоставляет страницу для кодов состояния ошибок HTTP, таких как код 404 Not Found (не найдено). Когда в приложении устанавливается код состояния ошибки HTTP 400–599 без текста, возвращается код состояния и пустой текст ответа. Чтобы включить обработчики по умолчанию, возвращающие только текст для распространенных кодов состояния ошибки, вызовите UseStatusCodePages в Program.cs:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages();

Вызовите UseStatusCodePages до ПО промежуточного слоя для обработки запросов. Например, вызовите UseStatusCodePages до ПО промежуточной обработки статических файлов и ПО промежуточной обработки конечных точек.

Если UseStatusCodePages не используется, при переходе по URL-адресу без конечной точки возвращается зависящее от браузера сообщение об ошибке, в котором указывается, что конечная точка не найдена. При вызове метода UseStatusCodePages браузер вернет следующий ответ:

Status Code: 404; Not Found

UseStatusCodePages обычно не применяется в рабочей среде, так как возвращает сообщение, бесполезное для пользователей.

Note

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

UseStatusCodePages со строкой формата

Чтобы настроить тип содержимого и текст ответа, используйте перегрузку UseStatusCodePages, которая принимает тип содержимого и строку форматирования.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");

В предыдущем коде {0} служит заполнителем для кода ошибки.

UseStatusCodePages со строкой формата обычно не применяется в рабочей среде, так как возвращает сообщение, бесполезное для пользователей.

UseStatusCodePages с использованием лямбда-выражения

Чтобы указать пользовательский код обработки ошибок и записи ответа, используйте перегрузку UseStatusCodePages, которая принимает лямбда-выражение.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages(async statusCodeContext =>
{
    // using static System.Net.Mime.MediaTypeNames;
    statusCodeContext.HttpContext.Response.ContentType = Text.Plain;

    await statusCodeContext.HttpContext.Response.WriteAsync(
        $"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});

UseStatusCodePages с функцией Lambda обычно не используется в рабочей среде, так как возвращает сообщение, не представляющее пользы для пользователей.

UseStatusCodePagesWithRedirects

Метод расширения UseStatusCodePagesWithRedirects:

  • Отправляет клиенту код состояния 302 — Found.
  • Перенаправляет клиент в конечную точку обработки ошибок, указанную в шаблоне URL-адреса. Конечная точка обработки ошибок обычно отображает сведения об ошибке и возвращает код HTTP 200.
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");

Шаблон URL-адреса может содержать заполнитель {0} для кода состояния, как показано в предыдущем коде. Если шаблон URL-адреса начинается с ~ (тильды), ~ заменяется на PathBase приложения. При указании конечной точки в приложении создайте представление MVC или страницу Razor для конечной точки.

Этот метод обычно используется, если приложение:

  • Должно перенаправлять клиент в другую конечную точку, что обычно бывает в случаях, когда другое приложение обрабатывает ошибку. Для веб-приложений в адресной строке браузера клиента отображается конечная точка перенаправления.
  • Не следует сохранять и возвращать исходный код состояния в ответе первоначального перенаправления.

UseStatusCodePagesWithReExecute

Метод расширения UseStatusCodePagesWithReExecute:

  • Возвращает исходный код состояния клиенту.
  • Позволяет создать текст ответа путем повторного выполнения конвейера запросов с использованием другого пути.
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");

Если указывается конечная точка в приложении, создайте представление MVC или страницу Razor для конечной точки.

Этот метод обычно используется, если приложение:

  • Обрабатывает запрос без перенаправления к другой конечной точке. Для веб-приложений в адресной строке браузера клиента отображается изначально запрошенная конечная точка.
  • Сохраняет и возвращает исходный код состояния с ответом.

Шаблон URL-адреса должен начинаться с символа / и может содержать заполнитель {0} для кода состояния. Чтобы передать код состояния в качестве параметра строки запроса, передайте второй аргумент в UseStatusCodePagesWithReExecute. Рассмотрим пример.

app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");

Конечная точка, которая обрабатывает ошибку, может получать исходный URL-адрес, вызвавший ошибку, как показано в следующем примере:

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
    public int OriginalStatusCode { get; set; }

    public string? OriginalPathAndQuery { get; set; }

    public void OnGet(int statusCode)
    {
        OriginalStatusCode = statusCode;

        var statusCodeReExecuteFeature =
            HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

        if (statusCodeReExecuteFeature is not null)
        {
            OriginalPathAndQuery = string.Join(
                statusCodeReExecuteFeature.OriginalPathBase,
                statusCodeReExecuteFeature.OriginalPath,
                statusCodeReExecuteFeature.OriginalQueryString);
        }
    }
}

Отключение страниц с кодами состояния

Чтобы отключить страницы кодов состояния для метода контроллера или действия MVC, используйте атрибут [SkipStatusCodePages].

Чтобы отключить страницы кодов состояния для конкретных запросов в методе обработчика Razor Pages или в контроллере MVC, используйте IStatusCodePagesFeature.

public void OnGet()
{
    var statusCodePagesFeature =
        HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature is not null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}

Код обработки исключений

Код на страницах обработки исключений также может создавать исключения. Рабочие страницы ошибок необходимо тщательно тестировать, чтобы они не создавали собственных исключений.

Заголовки ответа

После отправки заголовков для ответа происходит следующее:

  • Приложение не может изменить код состояния ответа.
  • Нельзя запустить страницу или обработчик исключений. Необходимо завершить ответ или прервать подключение.

Обработка исключений на сервере

Помимо логики обработки исключений в приложении, реализация HTTP-сервера также может обрабатывать некоторые исключения. Если сервер перехватывает исключение перед отправкой заголовков ответа, сервер отправляет 500 - Internal Server Error ответ без текста ответа. Если сервер перехватывает исключение после отправки заголовков ответа, он закрывает соединение. Запросы, не обработанные приложением, обрабатываются сервером. Все исключения, возникшие при обработке запроса серверов, обрабатываются с помощью механизма обработки исключений на сервере. Пользовательские страницы ошибок приложения, компоненты промежуточного ПО для обработки исключений и фильтры не влияют на это поведение.

Обработка исключений при запуске

Исключения, возникающие во время запуска приложения, могут обрабатываться только на уровне размещения. Узел можно настроить для перехвата ошибок при загрузке и записи подробных сведений об ошибках.

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

  • Уровень хостинга регистрирует в журнале критическое исключение.
  • Процесс dotnet аварийно завершается.
  • Если приложение запущено на HTTP-сервере Kestrel, страница со сведениями об ошибке не отображается.

При работе в службах IIS (или Службе приложений Azure) либо IIS Expressмодуль ASP.NET Core возвращает ошибку 502.5 Process Failure (ошибка процесса), если процесс невозможно запустить. Дополнительные сведения см. в статье Устранение неполадок с ASP.NET Core в Службе приложений Azure и IIS.

Страница ошибок базы данных

Фильтр исключений для страницы разработчика базы данных AddDatabaseDeveloperPageExceptionFilter перехватывает исключения, относящиеся к базе данных, которые могут быть устранены с помощью миграций Entity Framework Core. При возникновении этих исключений формируется HTML-ответ с подробными сведениями о возможных действиях для устранения проблемы. Эта страница включена только в Development среде. В следующем коде добавляется фильтр исключений для страницы разработчика базы данных:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();

Фильтры исключений

В приложениях MVC фильтры исключений можно настраивать как глобально, так и для отдельных контроллеров или действий. В приложениях Razor Pages они могут быть настроены глобально или для модели страницы. Эти фильтры обрабатывают все необработанные исключения, которые возникают во время выполнения действия контроллера или другого фильтра. Дополнительные сведения см. в статье Фильтры в ASP.NET Core.

Фильтры исключений полезны при перехвате исключений, которые возникают в действиях MVC. Однако эти фильтры не так гибки, как встроенное ПО промежуточного слоя для обработки исключенийUseExceptionHandler. Мы рекомендуем использовать UseExceptionHandler, если ошибки не нужно обрабатывать по-разному в зависимости от выбранного действия MVC.

Ошибки состояния модели

Сведения о том, как обрабатывать ошибки состояния модели, см. в статьях о привязке модели и проверке модели.

Дополнительные ресурсы

Авторы: Кирк Ларкин (Kirk Larkin), Том Дикстра (Tom Dykstra) и Стив Смит (Steve Smith)

В этой статье рассматриваются основные методы обработки ошибок в веб-приложениях ASP.NET Core. См. Обработка ошибок в API ASP.NET Core для веб-API.

Просмотреть или скачать образец кода. (Как скачать.) Сетевая вкладка в средствах разработчика браузера F12 полезна при тестировании примера приложения.

Страница исключений разработчика

Страница исключений для разработчика содержит подробные сведения о необработанных исключениях запросов. Шаблоны ASP.NET Core создают следующий код:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

Предыдущий выделенный код включает страницу исключений разработчика при запуске приложения в Development среде.

Шаблоны размещают UseDeveloperExceptionPage в начале конвейера промежуточного ПО, чтобы он мог перехватывать необработанные исключения, выбрасываемые в расположенном далее промежуточном ПО.

Приведенный выше код включает страницу исключений разработчика , только если приложение выполняется в Development среде. Подробные сведения об исключении не должны отображаться публично при запуске приложения в Production среде. Дополнительные сведения о настройке сред см. в ASP.NET средах выполнения Core.

Страница исключений для разработчика может содержать следующие сведения об исключении и запросе:

  • Трассировка стека
  • параметры строки запроса (при наличии);
  • Cookies Если таковые есть
  • Headers

Страница исключений для разработчика не обязательно содержит какую-либо информацию. Используйте Ведение журнала для получения полных сведений об ошибке.

Страница обработчика исключений

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

  • Перехватывает и записывает в журнал необработанные исключения.
  • повторно выполняет запрос в альтернативном конвейере по указанному пути. Запрос не выполняется повторно, если запущен отклик. Созданный шаблоном код повторно выполняет запрос, используя путь /Error.

Warning

Если альтернативный конвейер вызывает собственное исключение, middleware обработки исключений повторно выбрасывает исходное исключение.

В следующем примере UseExceptionHandler добавляет Middleware обработки исключений в не-Development средах.

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Шаблон приложения Razor Pages предоставляет страницу ошибки (.cshtml) и класс PageModel (ErrorModel) в папке Pages. Для приложения MVC шаблон проекта содержит метод действия Error и представление ошибок для контроллера Home.

ПО промежуточного слоя обработки исключений повторно выполняет запрос, используя исходный метод HTTP. Если конечная точка обработчика ошибок ограничена определенным набором методов HTTP, она выполняется только для этих методов HTTP. Например, действие контроллера MVC, использующее атрибут [HttpGet], выполняется только для запросов GET. Чтобы гарантировать, что все запросы будут попадать на страницу пользовательской обработки ошибок, не ограничивайте их определённым набором HTTP-методов.

Для избирательного управления исключениями в зависимости от исходного метода HTTP:

  • Для Razor страниц создайте несколько методов-обработчиков. Например, используйте OnGet, чтобы обрабатывать исключения GET, и OnPost, чтобы обрабатывать исключения POST.
  • Для MVC примените атрибуты HTTP-команды к нескольким действиям. Например, используйте [HttpGet], чтобы обрабатывать исключения GET, и [HttpPost], чтобы обрабатывать исключения POST.

Чтобы разрешить пользователям, не прошедшим проверку подлинности, просматривать страницу пользовательской обработки ошибок, убедитесь, что она поддерживает анонимный доступ.

Откройте исключение

Используйте интерфейс IExceptionHandlerPathFeature, чтобы получить доступ к исключению и к пути исходного запроса в обработчике ошибок. Следующий код добавляет ExceptionMessage к стандартному Pages/Error.cshtml.cs, созданному шаблонами ASP.NET Core:

[ResponseCache(Duration=0, Location=ResponseCacheLocation.None, NoStore=true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string RequestId { get; set; }
    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
    public string ExceptionMessage { get; set; }
    private readonly ILogger<ErrorModel> _logger;

    public ErrorModel(ILogger<ErrorModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
        HttpContext.Features.Get<IExceptionHandlerPathFeature>();
        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "File error thrown";
            _logger.LogError(ExceptionMessage);
        }
        if (exceptionHandlerPathFeature?.Path == "/index")
        {
            ExceptionMessage += " from home page";
        }
    }
}

Warning

Не передавайте клиентам конфиденциальную информацию об ошибках. Сохранение ошибок создает риски для безопасности.

Чтобы протестировать исключение в примере приложения, выполните указанные ниже действия.

  • Установите для среды продуктивный режим.
  • Удалите комментарии из webBuilder.UseStartup<Startup>(); в Program.cs.
  • Выберите "Активировать исключение" на домашней странице.

Лямбда-обработчик исключений

Альтернативой пользовательской странице обработчика исключений является предоставление лямбда-функции для UseExceptionHandler. Использование лямбда-функции позволяет получить доступ к ошибке до возврата ответа.

В следующем коде для обработки исключений используется лямбда-выражение:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler(errorApp =>
        {
            errorApp.Run(async context =>
            {
                context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;;
                context.Response.ContentType = "text/html";

                await context.Response.WriteAsync("<html lang=\"en\"><body>\r\n");
                await context.Response.WriteAsync("ERROR!<br><br>\r\n");

                var exceptionHandlerPathFeature =
                    context.Features.Get<IExceptionHandlerPathFeature>();

                if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
                {
                    await context.Response.WriteAsync(
                                              "File error thrown!<br><br>\r\n");
                }

                await context.Response.WriteAsync(
                                              "<a href=\"/\">Home</a><br>\r\n");
                await context.Response.WriteAsync("</body></html>\r\n");
                await context.Response.WriteAsync(new string(' ', 512)); 
            });
        });
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

Warning

Не передавайте клиентам конфиденциальную информацию об ошибках из IExceptionHandlerFeature или IExceptionHandlerPathFeature. Сохранение ошибок создает риски для безопасности.

Чтобы протестировать лямбда-функцию обработки исключений в приложении sample app:

  • Установите для среды продуктивный режим.
  • Удалите комментарии из webBuilder.UseStartup<StartupLambda>(); в Program.cs.
  • Выберите "Активировать исключение" на домашней странице.

UseStatusCodePages

По умолчанию приложение ASP.NET Core не предоставляет страницу для кодов состояния ошибок HTTP, таких как код 404 Not Found (не найдено). Когда в приложении устанавливается код состояния ошибки HTTP 400–599 без текста, возвращается код состояния и пустой текст ответа. Чтобы предоставить страницы кодов состояния, используйте ПО промежуточного слоя для страниц кодов состояния. Чтобы включить обработчики по умолчанию, возвращающие только текст для распространенных кодов состояния ошибки, вызовите UseStatusCodePages в методе Startup.Configure.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePages();

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

Вызовите UseStatusCodePages до ПО промежуточного слоя для обработки запросов. Например, вызовите UseStatusCodePages до ПО промежуточной обработки статических файлов и ПО промежуточной обработки конечных точек.

Если UseStatusCodePages не используется, при переходе по URL-адресу без конечной точки возвращается зависящее от браузера сообщение об ошибке, в котором указывается, что конечная точка не найдена. Например, перейти по адресу Home/Privacy2. В этом случае при вызове UseStatusCodePages браузер вернет следующее сообщение:

Status Code: 404; Not Found

UseStatusCodePages обычно не применяется в рабочей среде, так как возвращает сообщение, бесполезное для пользователей.

Чтобы протестировать UseStatusCodePages в примере приложения, выполните указанные ниже действия.

  • Установите для среды продуктивный режим.
  • Удалите комментарии из webBuilder.UseStartup<StartupUseStatusCodePages>(); в Program.cs.
  • Выберите ссылки на домашней странице.

Note

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

UseStatusCodePages со строкой формата

Чтобы настроить тип содержимого и текст ответа, используйте перегрузку UseStatusCodePages, которая принимает тип содержимого и строку форматирования.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePages(
        "text/plain", "Status code page, status code: {0}");

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

В предыдущем коде {0} служит заполнителем для кода ошибки.

UseStatusCodePages со строкой формата обычно не применяется в рабочей среде, так как возвращает сообщение, бесполезное для пользователей.

Чтобы протестировать UseStatusCodePages в примере приложения, удалите комментарии из webBuilder.UseStartup<StartupFormat>(); в Program.cs.

UseStatusCodePages с использованием лямбда-выражения

Чтобы указать пользовательский код обработки ошибок и записи ответа, используйте перегрузку UseStatusCodePages, которая принимает лямбда-выражение.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePages(async context =>
    {
        context.HttpContext.Response.ContentType = "text/plain";

        await context.HttpContext.Response.WriteAsync(
            "Status code page, status code: " +
            context.HttpContext.Response.StatusCode);
    });

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

UseStatusCodePages с функцией Lambda обычно не используется в рабочей среде, так как возвращает сообщение, не представляющее пользы для пользователей.

Чтобы протестировать UseStatusCodePages в примере приложения, удалите комментарии из webBuilder.UseStartup<StartupStatusLambda>(); в Program.cs.

UseStatusCodePagesWithRedirects

Метод расширения UseStatusCodePagesWithRedirects:

  • Отправляет клиенту код состояния 302 — Found.
  • Перенаправляет клиент в конечную точку обработки ошибок, указанную в шаблоне URL-адреса. Конечная точка обработки ошибок обычно отображает сведения об ошибке и возвращает код HTTP 200.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePagesWithRedirects("/MyStatusCode?code={0}");

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

Шаблон URL-адреса может содержать заполнитель {0} для кода состояния, как показано в предыдущем коде. Если шаблон URL-адреса начинается с ~ (тильды), ~ заменяется на PathBase приложения. При указании конечной точки в приложении создайте представление MVC или страницу Razor для конечной точки. Пример Razor Pages доступен в примере приложения в файле Pages/MyStatusCode.cshtml.

Этот метод обычно используется, если приложение:

  • Должно перенаправлять клиент в другую конечную точку, что обычно бывает в случаях, когда другое приложение обрабатывает ошибку. Для веб-приложений в адресной строке браузера клиента отображается конечная точка перенаправления.
  • Не следует сохранять и возвращать исходный код состояния в ответе первоначального перенаправления.

Чтобы протестировать UseStatusCodePages в примере приложения, удалите комментарии из webBuilder.UseStartup<StartupSCredirect>(); в Program.cs.

UseStatusCodePagesWithReExecute

Метод расширения UseStatusCodePagesWithReExecute:

  • Возвращает исходный код состояния клиенту.
  • Позволяет создать текст ответа путем повторного выполнения конвейера запросов с использованием другого пути.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePagesWithReExecute("/MyStatusCode2", "?code={0}");

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

Если указывается конечная точка в приложении, создайте представление MVC или страницу Razor для конечной точки. Обязательно поместите UseStatusCodePagesWithReExecute перед UseRouting, чтобы запрос можно было перенаправить на страницу состояния. Пример Razor Pages доступен в примере приложения в файле Pages/MyStatusCode2.cshtml.

Этот метод обычно используется, если приложение:

  • Обрабатывает запрос без перенаправления к другой конечной точке. Для веб-приложений в адресной строке браузера клиента отображается изначально запрошенная конечная точка.
  • Сохраняет и возвращает исходный код состояния с ответом.

Шаблоны URL-адреса и строки запроса могут содержать заполнитель {0} для кода состояния. Шаблон URL-адреса должен начинаться с символа /.

Конечная точка, которая обрабатывает ошибку, может получать исходный URL-адрес, вызвавший ошибку, как показано в следующем примере:

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class MyStatusCode2Model : PageModel
{
    public string RequestId { get; set; }
    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string ErrorStatusCode { get; set; }

    public string OriginalURL { get; set; }
    public bool ShowOriginalURL => !string.IsNullOrEmpty(OriginalURL);

    public void OnGet(string code)
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
        ErrorStatusCode = code;

        var statusCodeReExecuteFeature = HttpContext.Features.Get<
                                               IStatusCodeReExecuteFeature>();
        if (statusCodeReExecuteFeature != null)
        {
            OriginalURL =
                statusCodeReExecuteFeature.OriginalPathBase
                + statusCodeReExecuteFeature.OriginalPath
                + statusCodeReExecuteFeature.OriginalQueryString;
        }
    }
}

Пример Razor Pages доступен в примере приложения в файле Pages/MyStatusCode2.cshtml.

Чтобы протестировать UseStatusCodePages в примере приложения, удалите комментарии из webBuilder.UseStartup<StartupSCreX>(); в Program.cs.

Отключение страниц с кодами состояния

Чтобы отключить страницы кодов состояния для метода контроллера или действия MVC, используйте атрибут [SkipStatusCodePages].

Чтобы отключить страницы кодов состояния для конкретных запросов в методе обработчика Razor Pages или в контроллере MVC, используйте IStatusCodePagesFeature.

public void OnGet()
{
    // using Microsoft.AspNetCore.Diagnostics;
    var statusCodePagesFeature = HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature != null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}

Код обработки исключений

Код на страницах обработки исключений также может создавать исключения. Рабочие страницы ошибок необходимо тщательно тестировать, чтобы они не создавали собственных исключений.

Заголовки ответа

После отправки заголовков для ответа происходит следующее:

  • Приложение не может изменить код состояния ответа.
  • Нельзя запустить страницу или обработчик исключений. Необходимо завершить ответ или прервать подключение.

Обработка исключений на сервере

Помимо логики обработки исключений в приложении, реализация HTTP-сервера также может обрабатывать некоторые исключения. Если сервер перехватывает исключение перед отправкой заголовков ответа, сервер отправляет 500 - Internal Server Error ответ без текста ответа. Если сервер перехватывает исключение после отправки заголовков ответа, он закрывает соединение. Запросы, не обработанные приложением, обрабатываются сервером. Все исключения, возникшие при обработке запроса серверов, обрабатываются с помощью механизма обработки исключений на сервере. Пользовательские страницы ошибок приложения, компоненты промежуточного ПО для обработки исключений и фильтры не влияют на это поведение.

Обработка исключений при запуске

Исключения, возникающие во время запуска приложения, могут обрабатываться только на уровне размещения. Узел можно настроить для перехвата ошибок при загрузке и записи подробных сведений об ошибках.

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

  • Уровень хостинга регистрирует в журнале критическое исключение.
  • Процесс dotnet аварийно завершается.
  • Если приложение запущено на HTTP-сервере Kestrel, страница со сведениями об ошибке не отображается.

При работе в службах IIS (или Службе приложений Azure) либо IIS Expressмодуль ASP.NET Core возвращает ошибку 502.5 Process Failure (ошибка процесса), если процесс невозможно запустить. Дополнительные сведения см. в статье Устранение неполадок с ASP.NET Core в Службе приложений Azure и IIS.

Страница ошибок базы данных

Фильтр исключений для страницы разработчика базы данных AddDatabaseDeveloperPageExceptionFilter перехватывает исключения, относящиеся к базе данных, которые могут быть устранены с помощью миграций Entity Framework Core. При возникновении этих исключений формируется HTML-ответ с подробными сведениями о возможных действиях для устранения проблемы. Эта страница включена только в Development среде. Следующий код был создан шаблонами страниц Razor в ASP.NET Core при указании отдельных учетных записей пользователей:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDatabaseDeveloperPageExceptionFilter();
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddRazorPages();
}

Фильтры исключений

В приложениях MVC фильтры исключений можно настраивать как глобально, так и для отдельных контроллеров или действий. В приложениях Razor Pages они могут быть настроены глобально или для модели страницы. Эти фильтры обрабатывают все необработанные исключения, которые возникают во время выполнения действия контроллера или другого фильтра. Дополнительные сведения см. в статье Фильтры в ASP.NET Core.

Фильтры исключений полезны при перехвате исключений, которые возникают в действиях MVC. Однако эти фильтры не так гибки, как встроенное ПО промежуточного слоя для обработки исключенийUseExceptionHandler. Мы рекомендуем использовать UseExceptionHandler, если ошибки не нужно обрабатывать по-разному в зависимости от выбранного действия MVC.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

Ошибки состояния модели

Сведения о том, как обрабатывать ошибки состояния модели, см. в статьях о привязке модели и проверке модели.

Дополнительные ресурсы

Авторы: Том Дикстра (Tom Dykstra) и Стив Смит (Steve Smith)

В этой статье рассматриваются основные методы обработки ошибок в веб-приложениях ASP.NET Core. См. Обработка ошибок в API ASP.NET Core для веб-API.

Просмотреть или скачать образец кода. Как скачать.

Страница исключений разработчика

Страница исключений для разработчика содержит подробные сведения об исключениях запросов. Шаблоны ASP.NET Core создают следующий код:

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Приведенный выше код включает страницу исключений разработчика при запуске приложения в Development среде.

Шаблоны помещают метод UseDeveloperExceptionPage перед всем ПО промежуточного слоя, чтобы исключения перехватывались в этом ПО.

Приведенный выше код включает страницу исключений разработчика только в том случае, если приложение работает в Development среде. Подробные сведения об исключениях не должны быть общедоступными при выполнении приложения в рабочей среде. Дополнительные сведения о настройке сред см. в ASP.NET средах выполнения Core.

Страница исключений для разработчика содержит следующие сведения об исключении и запросе:

  • Трассировка стека
  • параметры строки запроса (при наличии);
  • Cookies Если таковые есть
  • Headers

Страница обработчика исключений

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

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

В следующем примере UseExceptionHandler добавляет Middleware обработки исключений в не-Development средах.

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Шаблон приложения Razor Pages предоставляет страницу ошибки (.cshtml) и класс PageModel (ErrorModel) в папке Pages. Для приложения MVC шаблон проекта содержит метод действия Error и представление ошибок для контроллера Home.

Не следует помечать метод действия обработки ошибок атрибутами метода HTTP, например HttpGet. Из-за использования явных команд некоторые запросы могут не передаваться в метод. Разрешите анонимный доступ к методу, если не прошедшие проверку подлинности пользователи должны видеть представление ошибок.

Откройте исключение

Используйте интерфейс IExceptionHandlerPathFeature, чтобы получить доступ к исключению и к пути исходного запроса в контроллере или на странице обработчика ошибок:

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class ErrorModel : PageModel
{
    public string RequestId { get; set; }
    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
    public string ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();
        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "File error thrown";
        }
        if (exceptionHandlerPathFeature?.Path == "/index")
        {
            ExceptionMessage += " from home page";
        }
    }
}

Warning

Не передавайте клиентам конфиденциальную информацию об ошибках. Сохранение ошибок создает риски для безопасности.

Чтобы вызвать описанную выше страницу обработки исключений, установите среду в режим production и принудительно вызовите исключение.

Лямбда-обработчик исключений

Альтернативой пользовательской странице обработчика исключений является предоставление лямбда-функции для UseExceptionHandler. Использование лямбда-функции позволяет получить доступ к ошибке до возврата ответа.

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

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
   app.UseExceptionHandler(errorApp =>
   {
        errorApp.Run(async context =>
        {
            context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
            context.Response.ContentType = "text/html";

            await context.Response.WriteAsync("<html lang=\"en\"><body>\r\n");
            await context.Response.WriteAsync("ERROR!<br><br>\r\n");

            var exceptionHandlerPathFeature = 
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync("File error thrown!<br><br>\r\n");
            }

            await context.Response.WriteAsync("<a href=\"/\">Home</a><br>\r\n");
            await context.Response.WriteAsync("</body></html>\r\n");
            await context.Response.WriteAsync(new string(' ', 512)); // IE padding
        });
    });
    app.UseHsts();
}

В приведенном выше коде добавляется await context.Response.WriteAsync(new string(' ', 512));, поэтому браузер Internet Explorer отображает сообщение об ошибке, а не сообщение об ошибке IE. Дополнительные сведения см. здесь на GitHub.

Warning

Не передавайте клиентам конфиденциальную информацию об ошибках из IExceptionHandlerFeature или IExceptionHandlerPathFeature. Сохранение ошибок создает риски для безопасности.

Чтобы увидеть результат обработки исключений с помощью лямбда-функции в примере приложения, используйте ProdEnvironment и ErrorHandlerLambda директивы препроцессора и выберите 'Активировать исключение' на домашней странице.

UseStatusCodePages

По умолчанию приложение ASP.NET Core не предоставляет страницы для кодов состояния HTTP, таких как код 404 Not Found (не найдено). Приложение возвращает код состояния без текста ответа. Чтобы предоставить страницы кодов состояния, используйте ПО промежуточного слоя Status Code Pages.

Это ПО промежуточного слоя доступно в пакете Microsoft.AspNetCore.Diagnostics.

Чтобы включить обработчики по умолчанию, возвращающие только текст для распространенных кодов состояния ошибки, вызовите UseStatusCodePages в методе Startup.Configure.

app.UseStatusCodePages();

Вызовите UseStatusCodePages перед ПО промежуточного слоя, обрабатывающим запросы (например, ПО промежуточного слоя для статических файлов и MVC).

Если UseStatusCodePages не используется, при переходе по URL-адресу без конечной точки возвращается зависящее от браузера сообщение об ошибке, в котором указывается, что конечная точка не найдена. Например, перейти по адресу Home/Privacy2. В этом случае при вызове UseStatusCodePages браузер вернет следующее сообщение:

Status Code: 404; Not Found

UseStatusCodePages со строкой формата

Чтобы настроить тип содержимого и текст ответа, используйте перегрузку UseStatusCodePages, которая принимает тип содержимого и строку форматирования.

app.UseStatusCodePages(
    "text/plain", "Status code page, status code: {0}");

UseStatusCodePages с использованием лямбда-выражения

Чтобы указать пользовательский код обработки ошибок и записи ответа, используйте перегрузку UseStatusCodePages, которая принимает лямбда-выражение.

app.UseStatusCodePages(async context =>
{
    context.HttpContext.Response.ContentType = "text/plain";

    await context.HttpContext.Response.WriteAsync(
        "Status code page, status code: " + 
        context.HttpContext.Response.StatusCode);
});

UseStatusCodePagesWithRedirects

Метод расширения UseStatusCodePagesWithRedirects:

  • Отправляет клиенту код состояния 302 — Found.
  • Перенаправляет клиента к расположению, предоставленному в шаблоне URL-адреса.
app.UseStatusCodePagesWithRedirects("/StatusCode?code={0}");

Шаблон URL-адреса может содержать заполнитель {0} для кода состояния, как показано в примере. Если шаблон URL-адреса начинается с ~ (тильды), ~ заменяется на PathBase приложения. Если вы указываете на конечную точку в приложении, создайте представление MVC или страницу Razor для конечной точки. Пример для Pages см. в RazorPages/StatusCode.cshtml.

Этот метод обычно используется, если приложение:

  • Должно перенаправлять клиент в другую конечную точку, что обычно бывает в случаях, когда другое приложение обрабатывает ошибку. Для веб-приложений в адресной строке браузера клиента отображается конечная точка перенаправления.
  • Не следует сохранять и возвращать исходный код состояния в ответе первоначального перенаправления.

UseStatusCodePagesWithReExecute

Метод расширения UseStatusCodePagesWithReExecute:

  • Возвращает исходный код состояния клиенту.
  • Позволяет создать текст ответа путем повторного выполнения конвейера запросов с использованием другого пути.
app.UseStatusCodePagesWithReExecute("/StatusCode","?code={0}");

Если вы указываете на конечную точку в приложении, создайте представление MVC или страницу Razor для конечной точки. Обязательно поместите UseStatusCodePagesWithReExecute перед UseRouting, чтобы запрос можно было перенаправить на страницу состояния. Пример для Pages см. в RazorPages/StatusCode.cshtml.

Этот метод обычно используется, если приложение:

  • Обрабатывает запрос без перенаправления к другой конечной точке. Для веб-приложений в адресной строке браузера клиента отображается изначально запрошенная конечная точка.
  • Сохраняет и возвращает исходный код состояния с ответом.

Шаблоны URL-адреса и строки запроса могут содержать заполнитель ({0}) для кода состояния. Шаблон URL-адреса должен начинаться с косой черты (/). При использовании заполнителя в пути убедитесь, что конечная точка (страница или контроллер) может обработать сегмент пути. Например, страница Razor ошибок должна принимать необязательное значение сегмента пути с директивой @page.

@page "{code?}"

Конечная точка, которая обрабатывает ошибку, может получать исходный URL-адрес, вызвавший ошибку, как показано в следующем примере:

var statusCodeReExecuteFeature = HttpContext.Features.Get<IStatusCodeReExecuteFeature>();
if (statusCodeReExecuteFeature != null)
{
    OriginalURL =
        statusCodeReExecuteFeature.OriginalPathBase
        + statusCodeReExecuteFeature.OriginalPath
        + statusCodeReExecuteFeature.OriginalQueryString;
}

Не следует помечать метод действия обработки ошибок атрибутами метода HTTP, например HttpGet. Из-за использования явных команд некоторые запросы могут не передаваться в метод. Разрешите анонимный доступ к методу, если не прошедшие проверку подлинности пользователи должны видеть представление ошибок.

Отключение страниц с кодами состояния

Чтобы отключить страницы кодов состояния для метода контроллера или действия MVC, используйте атрибут [SkipStatusCodePages].

Чтобы отключить страницы кодов состояния для конкретных запросов в методе обработчика Razor Pages или в контроллере MVC, используйте IStatusCodePagesFeature.

var statusCodePagesFeature = HttpContext.Features.Get<IStatusCodePagesFeature>();

if (statusCodePagesFeature != null)
{
    statusCodePagesFeature.Enabled = false;
}

Код обработки исключений

Код на страницах обработки исключений может создавать исключения. Часто желательно, чтобы страницы ошибок в рабочей среде содержали только статическое содержимое.

Заголовки ответа

После отправки заголовков для ответа происходит следующее:

  • Приложение не может изменить код состояния ответа.
  • Нельзя запустить страницу или обработчик исключений. Необходимо завершить ответ или прервать подключение.

Обработка исключений на сервере

Помимо логики обработки исключений в приложении, реализация HTTP-сервера может выполнять ряд операций по обработке исключений. Если сервер перехватывает исключение перед отправкой заголовков ответа, он отсылает ответ 500 Internal Server Error (внутренняя ошибка сервера) без текста ответа. Если сервер перехватывает исключение после отправки заголовков ответа, он закрывает соединение. Запросы, не обработанные приложением, обрабатываются сервером. Все исключения, возникшие при обработке запроса серверов, обрабатываются с помощью механизма обработки исключений на сервере. Пользовательские страницы ошибок приложения, компоненты промежуточного ПО для обработки исключений и фильтры не влияют на это поведение.

Обработка исключений при запуске

Исключения, возникающие во время запуска приложения, могут обрабатываться только на уровне размещения. Узел можно настроить для перехвата ошибок при загрузке и записи подробных сведений об ошибках.

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

  • Уровень хостинга регистрирует в журнале критическое исключение.
  • Процесс dotnet аварийно завершается.
  • Если приложение запущено на HTTP-сервере Kestrel, страница со сведениями об ошибке не отображается.

При работе в службах IIS (или Службе приложений Azure) либо IIS Expressмодуль ASP.NET Core возвращает ошибку 502.5 Process Failure (ошибка процесса), если процесс невозможно запустить. Дополнительные сведения см. в статье Устранение неполадок с ASP.NET Core в Службе приложений Azure и IIS.

Страница ошибок базы данных

ПО промежуточного слоя страницы ошибок базы данных фиксирует исключения, связанные с базой данных, которые можно устранить с помощью миграций Entity Framework. При возникновении этих исключений формируется HTML-ответ с подробными сведениями о возможных действиях для устранения проблемы. Эта страница должна быть включена только в среде Development. Включите страницу, добавив код в Startup.Configure.

if (env.IsDevelopment())
{
    app.UseDatabaseErrorPage();
}

UseDatabaseErrorPage требует пакет NuGet Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.

Фильтры исключений

В приложениях MVC фильтры исключений можно настраивать как глобально, так и для отдельных контроллеров или действий. В приложениях Razor Pages они могут быть настроены глобально или для модели страницы. Эти фильтры обрабатывают все необработанные исключения, которые возникают во время выполнения действия контроллера или другого фильтра. Дополнительные сведения см. в статье Фильтры в ASP.NET Core.

Tip

Фильтры исключений полезны для перехвата исключений, возникающих в действиях MVC, но они не так гибки, как middleware обработки исключений. Мы рекомендуем использовать ПО промежуточного слоя. Используйте фильтры, только если ошибки нужно обрабатывать по-разному в зависимости от выбранного действия MVC.

Ошибки состояния модели

Сведения о том, как обрабатывать ошибки состояния модели, см. в статьях о привязке модели и проверке модели.

Дополнительные ресурсы