
Как получить ответ веб-страницы через HttpWebRequest в ASP.NET
Иногда серверу нужно самому сходить на другой сайт: забрать HTML, курс валют или ответ API. Разбираем HttpWebRequest и подводные камни таких запросов.
Своя страница или чужая
Если нужная страница находится внутри того же приложения, ходить за ней по 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 разрешает всего два одновременных соединения к одному хосту. После пары «забытых» ответов следующие запросы начинают висеть до таймаута, и найти причину без этого знания очень сложно.
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 ничего не добавляет, его лучше убрать или заменить осмысленной обработкой: записать в лог адрес и код, вернуть пользователю понятное сообщение, показать закэшированную копию данных.
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 остаётся более предсказуемым выбором.
Как это делают сегодня
Вопросы и ответы
Почему запросы к одному сайту начинают зависать?
Скорее всего, где-то не закрыт HttpWebResponse. Соединения не возвращаются в пул, а лимит ServicePointManager.DefaultConnectionLimit по умолчанию равен двум.
Как отправить POST-запрос?
Установите Method = "POST", ContentType (например, application/x-www-form-urlencoded), запишите байты тела в GetRequestStream и только потом вызывайте GetResponse.
Почему вместо русского текста приходят непонятные символы?
Страница в Windows-1251, а читается как UTF-8. Берите кодировку из resp.CharacterSet или из meta charset в самом HTML.