HTTP клиент на C#: делаем запросы правильно — продвинутый уровень
В современной .NET-разработке работа с HTTP-запросами выходит далеко за рамки простого вызова WebClient или базового HttpClient. На продвинутом уровне необходимо учитывать управление пулом соединений, корректную обработку ошибок, таймауты, политики повторных попыток и асинхронное выполнение. В этой статье мы разберём лучшие практики работы с HTTP-клиентом на C#, которые помогут вам создавать надёжные и производительные приложения.
1. Правильная работа с HttpClient и пулом соединений
Одна из самых частых ошибок — создание нового экземпляра HttpClient на каждый запрос. Это приводит к истощению сокетов (socket exhaustion) из-за того, что каждый экземпляр держит открытое TCP-соединение в состоянии TIME_WAIT.
Неправильно:
public async Task<string> GetDataAsync(string url)
{
using (var client = new HttpClient())
{
return await client.GetStringAsync(url);
}
}
Правильно: используйте один статический или синглтон-экземпляр HttpClient.
public class HttpService
{
private static readonly HttpClient _client = new HttpClient();
public async Task<string> GetDataAsync(string url)
{
return await _client.GetStringAsync(url);
}
}
Для более тонкого контроля используйте IHttpClientFactory из пакета Microsoft.Extensions.Http. Фабрика управляет пулом и автоматически восстанавливает соединения при сбоях DNS.
// В Startup.cs или Program.cs
services.AddHttpClient("MyClient", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
});
// В сервисе
public class MyService
{
private readonly IHttpClientFactory _httpClientFactory;
public MyService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<string> GetDataAsync()
{
var client = _httpClientFactory.CreateClient("MyClient");
return await client.GetStringAsync("/data");
}
}
2. Таймауты и отмена запросов
Без таймаутов ваш клиент может зависнуть на неопределённое время. В HttpClient есть два механизма:
- Timeout — общий таймаут для всего запроса.
- CancellationToken — для отмены запроса извне (например, пользователь закрыл приложение).
Устанавливайте Timeout разумно. Для большинства API подойдёт 10–30 секунд.
var client = new HttpClient
{
Timeout = TimeSpan.FromSeconds(15)
};
// Пример с CancellationTokenSource
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try
{
var response = await client.GetAsync("https://slow-api.example.com", cts.Token);
}
catch (TaskCanceledException)
{
Console.WriteLine("Запрос отменён по таймауту.");
}
Важно: Timeout и CancellationToken работают совместно. Если CancellationToken срабатывает раньше — запрос отменяется быстрее.
3. Обработка ошибок и политика повторных попыток (Retry)
Сетевые сбои и временные ошибки сервера — обычное дело. Вместо того чтобы сразу падать, реализуйте политику повторных попыток с экспоненциальной задержкой. Для этого отлично подходит библиотека Polly.
using Polly;
using Polly.Extensions.Http;
// Определяем политику повторных попыток
var retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError() // 5xx, 408
.Or<HttpRequestException>()
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
// Применяем к клиенту
services.AddHttpClient("RetryClient")
.AddPolicyHandler(retryPolicy);
Также полезно добавить политику Circuit Breaker (автоматическое отключение вызовов при высокой частоте ошибок):
var circuitBreakerPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
services.AddHttpClient("ResilientClient")
.AddPolicyHandler(retryPolicy)
.AddPolicyHandler(circuitBreakerPolicy);
Не забывайте логировать ошибки. Используйте ILogger для записи деталей сбоя.
4. Продвинутая работа с заголовками и контентом
На продвинутом уровне часто требуется кастомная сериализация, работа с куками, аутентификация (Bearer, Basic) и потоковая передача данных.
Пример с Bearer-аутентификацией:
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "your_token_here");
// Или динамически для каждого запроса
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com/secure");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await client.SendAsync(request);
Потоковая передача больших данных:
using var response = await client.GetAsync("https://example.com/large-file",
HttpCompletionOption.ResponseHeadersRead); // не буферизировать сразу
using var stream = await response.Content.ReadAsStreamAsync();
// Читаем поток частями
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
// Обрабатываем chunk
}
Работа с куками:
var cookieContainer = new CookieContainer();
var handler = new HttpClientHandler
{
CookieContainer = cookieContainer,
UseCookies = true
};
var client = new HttpClient(handler);
// После запроса куки автоматически сохранятся
await client.GetAsync("https://example.com/login");
var cookies = cookieContainer.GetCookies(new Uri("https://example.com"));
Заключение
Правильная работа с HTTP-клиентом на C# — это не просто вызов метода GetAsync. Используйте IHttpClientFactory для управления пулом, настраивайте таймауты и отмену, внедряйте политики повторных попыток с помощью Polly, и не забывайте про поток