
Исключение при Response.Redirect: откуда ThreadAbortException и как его избежать
Редирект внутри блока Try в ASP.NET неожиданно попадает в Catch с сообщением «Thread was being aborted». Это не ошибка сервера, а штатное поведение Response.Redirect, и обходится оно одной строкой.
Симптом: редирект попадает в Catch
Типичный сценарий: обработчик кнопки сохраняет данные, а затем перенаправляет пользователя на страницу списка. Код аккуратно обёрнут в Try/Catch, ошибки пишутся в журнал. Через неделю журнал заполнен записями System.Threading.ThreadAbortException с текстом «Thread was being aborted», хотя пользователи ни на что не жалуются и редирект работает.
Хуже, если в блоке Catch не только журнал, но и реакция на ошибку: сообщение «Не удалось сохранить» или откат транзакции. Тогда штатный редирект превращается в ложную ошибку, а иногда и в потерю данных.
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, а рендер разметки, которую никто не увидит, лучше подавить.
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Решение 2: отдельный Catch для ThreadAbortException
Если кода с Response.Redirect(url) много и переписать всё сразу нельзя, можно хотя бы перестать считать прерывание ошибкой. Добавьте перед общим обработчиком отдельный Catch для ThreadAbortException и ничего в нём не делайте. Исключение всё равно возбудится повторно и завершит запрос, но журнал и сообщения пользователю останутся чистыми.
Этот вариант оставляет накладные расходы на исключения, поэтому подходит как временная заплатка при сопровождении старого кода, а не как основной приём.
Try
SaveOrder()
Response.Redirect("~/Orders.aspx")
Catch ex As Threading.ThreadAbortException
' Штатное завершение редиректа: не ошибка, ничего не делаем
Catch ex As Exception
Logger.Error(ex)
StatusLabel.Text = "Не удалось сохранить заказ"
End TryServer.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 тоже заметно снизится.
Как это делают сегодня
Вопросы и ответы
Можно ли просто подавить ThreadAbortException в Catch?
Погасить его нельзя: после Catch исключение возбуждается снова. Можно лишь не реагировать на него, но надёжнее использовать Redirect с параметром False.
Почему после Redirect с False код продолжает выполняться?
Потому что поток больше не прерывается. Сразу после редиректа вызывайте CompleteRequest и выходите из метода через Return.
Влияет ли выбор способа на поисковики?
Оба способа отдают код 302. Для постоянного переезда страницы используйте Response.RedirectPermanent, доступный с ASP.NET 4, он отдаёт код 301.