Исключение при Response.Redirect: откуда ThreadAbortException и как его избежать

РазделASP.NETУровеньСредний Чтение7 минОпубликовано ТегиASP.NETWeb FormsVB.NETИсключения

Редирект внутри блока Try в ASP.NET неожиданно попадает в Catch с сообщением «Thread was being aborted». Это не ошибка сервера, а штатное поведение Response.Redirect, и обходится оно одной строкой.

Главное. Response.Redirect(url) прерывает поток через Response.End и потому бросает ThreadAbortException. Вызывайте Response.Redirect(url, False) вместе с CompleteRequest, выходите из обработчика и не держите редирект внутри общего Try/Catch.

Симптом: редирект попадает в Catch

Типичный сценарий: обработчик кнопки сохраняет данные, а затем перенаправляет пользователя на страницу списка. Код аккуратно обёрнут в Try/Catch, ошибки пишутся в журнал. Через неделю журнал заполнен записями System.Threading.ThreadAbortException с текстом «Thread was being aborted», хотя пользователи ни на что не жалуются и редирект работает.

Хуже, если в блоке Catch не только журнал, но и реакция на ошибку: сообщение «Не удалось сохранить» или откат транзакции. Тогда штатный редирект превращается в ложную ошибку, а иногда и в потерю данных.

Проблемный кодVB.NET
Protected Sub SaveButton_Click(sender As Object, e As EventArgs) _
        Handles SaveButton.Click
    Try
        SaveOrder()
        Response.Redirect("~/Orders.aspx") ' бросит ThreadAbortException
    Catch ex As Exception
        ' Сюда попадает каждый успешный редирект
        Logger.Error(ex)
        StatusLabel.Text = "Не удалось сохранить заказ"
    End Try
End Sub

Почему так происходит

Метод Response.Redirect(url) с одним аргументом эквивалентен вызову Response.Redirect(url, True). Второй параметр endResponse означает «завершить обработку запроса немедленно». Для этого ASP.NET отправляет клиенту заголовок Location с кодом 302 и вызывает Response.End.

А Response.End в классическом ASP.NET останавливает выполнение страницы очень грубо: вызывает Thread.Abort для текущего потока. Среда бросает в нём ThreadAbortException, исключение раскручивает стек, и код после редиректа не выполняется. Особенность этого исключения в том, что его можно поймать, но нельзя погасить: после выхода из блока Catch оно возбуждается снова автоматически. Поэтому редирект в итоге всё равно срабатывает, но ваш Catch успевает выполниться.

То же самое делают Server.Transfer и прямой вызов Response.End. Кроме лишних записей в журнале, исключения ещё и дороги: на нагруженном сайте тысячи прерываний потоков в минуту заметно добавляют нагрузку на процессор.

Решение 1: endResponse False и CompleteRequest

Рекомендуемый способ: передать во втором параметре False, чтобы Response.Redirect не вызывал Response.End, а затем сообщить конвейеру ASP.NET, что запрос можно завершать, методом CompleteRequest. Исключения нет, заголовок 302 уходит клиенту как обычно.

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

Безопасный редиректVB.NET
Protected Sub SaveButton_Click(sender As Object, e As EventArgs) _
        Handles SaveButton.Click
    Try
        SaveOrder()
    Catch ex As SqlException
        Logger.Error(ex)
        StatusLabel.Text = "Не удалось сохранить заказ"
        Return
    End Try
    ' Редирект вне Try и без прерывания потока
    Response.Redirect("~/Orders.aspx", False)
    Context.ApplicationInstance.CompleteRequest()
    _redirecting = True
    Return ' код ниже не нужен
End Sub

Private _redirecting As Boolean

' Не тратим время на рендер страницы, которую браузер не покажет
Protected Overrides Sub Render(writer As HtmlTextWriter)
    If Not _redirecting Then MyBase.Render(writer)
End Sub
CompleteRequest не прерывает текущую страницу, а только велит конвейеру пропустить оставшиеся события приложения и перейти к EndRequest. Поэтому флаг и проверка в Render здесь не лишние.

Решение 2: отдельный Catch для ThreadAbortException

Если кода с Response.Redirect(url) много и переписать всё сразу нельзя, можно хотя бы перестать считать прерывание ошибкой. Добавьте перед общим обработчиком отдельный Catch для ThreadAbortException и ничего в нём не делайте. Исключение всё равно возбудится повторно и завершит запрос, но журнал и сообщения пользователю останутся чистыми.

Этот вариант оставляет накладные расходы на исключения, поэтому подходит как временная заплатка при сопровождении старого кода, а не как основной приём.

Временная заплаткаVB.NET
Try
    SaveOrder()
    Response.Redirect("~/Orders.aspx")
Catch ex As Threading.ThreadAbortException
    ' Штатное завершение редиректа: не ошибка, ничего не делаем
Catch ex As Exception
    Logger.Error(ex)
    StatusLabel.Text = "Не удалось сохранить заказ"
End Try

Server.Transfer, Response.End и другие источники

ThreadAbortException порождают все методы, которые внутри вызывают Response.End. Полезно знать их и их безопасные замены:

  • Response.End: вместо него используйте CompleteRequest и подавление рендера, как выше. Особенно часто Response.End встречается после выдачи файла в Response.BinaryWrite;
  • Server.Transfer(path): прерывает поток так же. Вариант Server.Execute выполняет другую страницу без прерывания, а в IIS 7 в интегрированном режиме есть Server.TransferRequest, который перезапускает запрос целиком;
  • FormsAuthentication.RedirectFromLoginPage внутри вызывает Redirect с endResponse True. Если нужен контроль, используйте SetAuthCookie и затем Redirect с False;
  • Response.Redirect в Global.asax или HttpModule ведёт себя так же: в событиях приложения после Redirect с False вызывайте CompleteRequest и выходите из обработчика.

Редирект после POST и проверка результата

Сам по себе редирект после сохранения формы это правильный приём, известный как Post/Redirect/Get. Без него обновление страницы в браузере повторно отправляет форму и может создать дубликат заказа. Поэтому отказываться от Response.Redirect не нужно, нужно лишь вызывать его без прерывания потока.

Проверить, что всё работает, просто: в инструментах разработчика браузера на вкладке сети у ответа на POST должен быть код 302 и заголовок Location, а в журнале приложения не должно появляться ThreadAbortException. Если вы используете счётчики производительности, показатель исключений в секунду для ASP.NET тоже заметно снизится.

Как это делают сегодня

В ASP.NET Core проблемы нет: Thread.Abort там не поддерживается, а редирект это просто результат действия. В MVC и Razor Pages пишут Return Redirect или RedirectToPage, в минимальных API используют Results.Redirect, а запрос завершается обычным возвратом из метода. При переносе Web Forms на .NET 8 такие места становятся проще и без побочных эффектов.

Вопросы и ответы

Можно ли просто подавить ThreadAbortException в Catch?

Погасить его нельзя: после Catch исключение возбуждается снова. Можно лишь не реагировать на него, но надёжнее использовать Redirect с параметром False.

Почему после Redirect с False код продолжает выполняться?

Потому что поток больше не прерывается. Сразу после редиректа вызывайте CompleteRequest и выходите из метода через Return.

Влияет ли выбор способа на поисковики?

Оба способа отдают код 302. Для постоянного переезда страницы используйте Response.RedirectPermanent, доступный с ASP.NET 4, он отдаёт код 301.

Читайте также