Как получить ответ веб-страницы через HttpWebRequest в ASP.NET

РазделASP.NETУровеньСредний Чтение7 минОпубликовано ТегиASP.NETVB.NETHTTP

Иногда серверу нужно самому сходить на другой сайт: забрать HTML, курс валют или ответ API. Разбираем HttpWebRequest и подводные камни таких запросов.

Главное. HttpWebRequest позволяет серверу забрать любую внешнюю страницу, но обязательно задавайте таймауты, закрывайте ответы через Using, учитывайте кодировку из заголовка и ловите WebException.

Своя страница или чужая

Если нужная страница находится внутри того же приложения, ходить за ней по HTTP не нужно. Server.Execute выполнит её в текущем контексте, с той же сессией и пользователем, а результат можно перехватить в StringWriter. Это быстрее и не требует сетевого обращения к самому себе.

Когда же источник внешний (сайт партнёра, сервис погоды, XML-фид), приходится выполнять полноценный HTTP-запрос с сервера. В .NET Framework для этого есть класс HttpWebRequest. Он создаётся фабричным методом WebRequest.Create, настраивается свойствами и возвращает HttpWebResponse с потоком ответа.

Настройка запроса

Главное свойство для GET-запроса это Timeout. По умолчанию он равен 100 секундам, и если удалённый сайт завис, ваш поток ASP.NET будет ждать всё это время, а пользователь увидит бесконечную загрузку. Для страниц разумно ставить 5-15 секунд. Отдельно есть ReadWriteTimeout, ограничивающий чтение тела ответа.

Многие сайты отвечают ошибкой или упрощённой страницей на запросы без заголовка User-Agent, поэтому его стоит заполнить. ContentType для GET задавать не нужно: он описывает тело запроса, а у GET тела нет. Свойство Method указывается заглавными буквами ("GET", "POST"), как требует протокол.

  • Timeout: общее время на установку соединения и получение заголовков.
  • ReadWriteTimeout: время на чтение тела ответа.
  • UserAgent: строка, по которой сайт узнаёт клиента.
  • AutomaticDecompression: включает прозрачную распаковку gzip и deflate.
  • Proxy: нужен, если сервер выходит в интернет через корпоративный прокси.

Чтение ответа и кодировка

Ответ читается через GetResponseStream, удобнее всего обернуть его в StreamReader и вызвать ReadToEnd. Кодировку не стоит жёстко прописывать как UTF-8: русские сайты нередко отдают Windows-1251. Сервер сообщает кодировку в заголовке Content-Type, а HttpWebResponse выдаёт её в свойстве CharacterSet. Если оно пустое, разумный запасной вариант UTF-8.

И HttpWebResponse, и поток нужно закрывать. Незакрытые ответы держат соединения из пула, а по умолчанию .NET разрешает всего два одновременных соединения к одному хосту. После пары «забытых» ответов следующие запросы начинают висеть до таймаута, и найти причину без этого знания очень сложно.

WebFetcher.vbVB.NET
Imports System.IO
Imports System.Net
Imports System.Text

Public Module WebFetcher
    Public Function DownloadPage(url As String) As String
        Dim req = DirectCast(WebRequest.Create(url), HttpWebRequest)
        req.Method = "GET"
        req.Timeout = 10000            ' 10 секунд на ответ
        req.ReadWriteTimeout = 10000
        req.UserAgent = "Mozilla/5.0 (compatible; MyApp/1.0)"
        req.AutomaticDecompression = DecompressionMethods.GZip Or _
                                     DecompressionMethods.Deflate

        Using resp = DirectCast(req.GetResponse(), HttpWebResponse)
            ' Кодировку берём из заголовка, иначе UTF-8
            Dim enc As Encoding = Encoding.UTF8
            If Not String.IsNullOrEmpty(resp.CharacterSet) Then
                enc = Encoding.GetEncoding(resp.CharacterSet)
            End If
            Using reader As New StreamReader(resp.GetResponseStream(), enc)
                Return reader.ReadToEnd()
            End Using
        End Using
    End Function
End Module

Ошибки HTTP и WebException

Важная особенность: при ответах 404 или 500 метод GetResponse не возвращает объект с соответствующим StatusCode, а бросает WebException. Проверка StatusCode = OK после вызова поэтому почти бесполезна, до неё дело просто не дойдёт. Код ответа и даже тело страницы ошибки можно достать из свойства Response исключения.

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

Обработка ошибокVB.NET
Try
    Dim html = DownloadPage("https://example.com/feed")
    litContent.Text = Server.HtmlEncode(html)
Catch ex As WebException When ex.Response IsNot Nothing
    Dim code = CType(ex.Response, HttpWebResponse).StatusCode
    lblError.Text = "Сайт ответил ошибкой: " & CInt(code)
Catch ex As WebException
    ' Таймаут, DNS, обрыв соединения
    lblError.Text = "Источник недоступен: " & ex.Status.ToString()
End Try

Безопасность и производительность

Если адрес приходит от пользователя, запрос с сервера превращается в классическую уязвимость SSRF: злоумышленник может заставить ваш сервер обратиться к внутренним ресурсам сети, например к панели администрирования на localhost. Разрешайте только заранее известные домены и схемы http/https.

Полученный HTML нельзя выводить на свою страницу как есть: вместе с ним приедут чужие скрипты. Либо экранируйте его через Server.HtmlEncode, либо разбирайте и берите только нужные данные. И наконец, результат внешних запросов почти всегда стоит кэшировать хотя бы на несколько минут: так страница не будет зависеть от скорости и доступности чужого сервера.

Когда достаточно WebClient

Для простого скачивания без тонкой настройки есть класс WebClient и его метод DownloadString. Он короче, но не даёт задать Timeout без наследования, и по умолчанию не учитывает кодировку из заголовка (её нужно выставить через свойство Encoding). Для серверного кода, где важны таймауты и контроль ошибок, HttpWebRequest остаётся более предсказуемым выбором.

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

В .NET 8 и HttpWebRequest, и WebClient помечены устаревшими: вместо них используется HttpClient с асинхронными методами GetStringAsync и GetAsync. В ASP.NET Core клиенты создаются через IHttpClientFactory, который правильно переиспользует соединения, а повторы и таймауты настраиваются политиками Polly или встроенным пакетом Microsoft.Extensions.Http.Resilience.

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

Почему запросы к одному сайту начинают зависать?

Скорее всего, где-то не закрыт HttpWebResponse. Соединения не возвращаются в пул, а лимит ServicePointManager.DefaultConnectionLimit по умолчанию равен двум.

Как отправить POST-запрос?

Установите Method = "POST", ContentType (например, application/x-www-form-urlencoded), запишите байты тела в GetRequestStream и только потом вызывайте GetResponse.

Почему вместо русского текста приходят непонятные символы?

Страница в Windows-1251, а читается как UTF-8. Берите кодировку из resp.CharacterSet или из meta charset в самом HTML.

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