Изменение параметров HttpClientHandler для одного HttpClient

Я делаю несколько запросов создавая для каждого HttpClient так

public class HttpRequest {
        public string Request(string method, string url, string content, string content_type, Dictionary<string, string> headers, string proxy, int timeout, bool auto_redirect, CookieContainer cookie_container) {
            WebProxy webProxy = new WebProxy();
            HttpClientHandler httpClientHandler = new HttpClientHandler();
            if (proxy.Length != 0) {
                string proxy_url = proxy.Split(new char[] { '@' })[0];
                webProxy = new WebProxy(proxy_url);
                if (proxy.Split(new char[] { '@' }).Length == 2) {
                    string proxy_auth = proxy.Split(new char[] { '@' })[1];
                    if (proxy_auth.Split(new char[] { ':' }).Length == 2) {
                        webProxy.Credentials = new NetworkCredential(proxy_auth.Split(new char[] { ':' })[0], proxy_auth.Split(new char[] { ':' })[1]);
                    }
                }

                httpClientHandler.UseProxy = true;
                httpClientHandler.Proxy = webProxy;
            }

            httpClientHandler.AllowAutoRedirect = auto_redirect;
            httpClientHandler.CookieContainer = cookie_container;

            HttpClient httpClient = new HttpClient(httpClientHandler);
             
            var request = new HttpRequestMessage();
            request.RequestUri = new Uri(url);
                
            foreach (var item in headers) {
                if (!request.Headers.TryAddWithoutValidation(item.Key, item.Value)) {
                    Console.WriteLine($"Не удалось добавить заголовок в запрос ({item.Key} = {item.Value})");
                }
            }

            //httpClient.DefaultRequestHeaders.Add("Connection", "keep-alive");

            httpClient.Timeout = TimeSpan.FromMilliseconds(timeout);

            if (method == "GET") {
                request.Method = HttpMethod.Get;
            } else if (method == "POST") {
                request.Method = HttpMethod.Post;
                    
                var content_dict = content.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries)
                    .Select(part => part.Split('='))
                    .ToDictionary(split => split[0], split => Uri.UnescapeDataString(split[1]));

                request.Content = new FormUrlEncodedContent(content_dict);
                request.Content.Headers.ContentType = new MediaTypeWithQualityHeaderValue(content_type);
            }

            HttpResponseMessage httpResponseMessage = httpClient.Send(request);
            var responseString = httpResponseMessage.Content.ReadAsStringAsync();

            return responseString.Result;
        }
}

Но возникает проблема утечки соединений.

Я попытался создать один экземпляр HttpClient, чтобы это исправить.

public class HttpRequest {
        public static HttpClientHandler httpClientHandler = new HttpClientHandler();
        public static HttpClient httpClient = new HttpClient(httpClientHandler);
        public string Request(string method, string url, string content, string content_type, Dictionary<string, string> headers, string proxy, int timeout, bool auto_redirect, CookieContainer cookie_container) {
            WebProxy webProxy = new WebProxy();
                
            if (proxy.Length != 0) {
                string proxy_url = proxy.Split(new char[] { '@' })[0];
                webProxy = new WebProxy(proxy_url);
                if (proxy.Split(new char[] { '@' }).Length == 2) {
                    string proxy_auth = proxy.Split(new char[] { '@' })[1];
                    if (proxy_auth.Split(new char[] { ':' }).Length == 2) {
                        webProxy.Credentials = new NetworkCredential(proxy_auth.Split(new char[] { ':' })[0], proxy_auth.Split(new char[] { ':' })[1]);
                    }
                }

                httpClientHandler.UseProxy = true;
                httpClientHandler.Proxy = webProxy;
            }

            httpClientHandler.AllowAutoRedirect = auto_redirect;
            httpClientHandler.CookieContainer = cookie_container;

            var request = new HttpRequestMessage();
            request.RequestUri = new Uri(url);
                
            foreach (var item in headers) {
                if (!request.Headers.TryAddWithoutValidation(item.Key, item.Value)) {
                    Console.ForegroundColor = ConsoleColor.Red;
                    Console.WriteLine($"Не удалось добавить заголовок в запрос ({item.Key} = {item.Value})");
                }
            }

            //httpClient.DefaultRequestHeaders.Add("Connection", "keep-alive");

            httpClient.Timeout = TimeSpan.FromMilliseconds(timeout);

            if (method == "GET") {
                request.Method = HttpMethod.Get;
            } else if (method == "POST") {
                request.Method = HttpMethod.Post;
                    
                var content_dict = content.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries)
                    .Select(part => part.Split('='))
                    .ToDictionary(split => split[0], split => Uri.UnescapeDataString(split[1]));

                request.Content = new FormUrlEncodedContent(content_dict);
                request.Content.Headers.ContentType = new MediaTypeWithQualityHeaderValue(content_type);
            }

            HttpResponseMessage httpResponseMessage = httpClient.Send(request);
            var responseString = httpResponseMessage.Content.ReadAsStringAsync();

            return responseString.Result;
        }
}

Тут тоже возникла проблема, HttpClientHandler можно задать один раз и если мне нужно изменять прокси и прочие параметры под каждый запрос, то у меня не получится это сделать, будет ошибка This instance has already started one or more requests. Properties can only be modified before sending the first request. Может быть есть пути решения этой проблемы?


Ответы (1 шт):

Автор решения: aepot
  • Про асинхронность забыли. Асинхронное программирование.
  • При использовании прокси нужно создавать отдельный клиент на каждую прокси. Перенастраивать прокси на уже созданном клиенте невозможно. Но и создавать из-за этого клиент на каждый запрос не следует.
  • Не используйте строки типа "GET", когда есть перечисление, так будет сложнее ошибиться.
  • Для FormUrlEncodedContent не надо задавать ContentType, этот класс сам его задаст правильно.

Создайте пул клиентов на свой список прокси и отправляйте по несколько запросов через каждый.

Пример кода:

public class HttpRequest : IDisposable
{
    private readonly HttpClient httpClient;

    public HttpRequest(string proxy, int timeout, bool autoRedirect = true, CookieContainer cookieContainer = null)
    {
        HttpClientHandler handler = new HttpClientHandler();

        if (proxy?.Length > 0)
        {
            string[] tokens = proxy.Split('@');
            string proxy_url = tokens[0];
            WebProxy webProxy = new WebProxy(proxy_url);
            if (tokens.Length == 2)
            {
                string proxy_auth = tokens[1];
                string[] authTokens = proxy_auth.Split(':');
                if (authTokens.Length == 2)
                {
                    webProxy.Credentials = new NetworkCredential(authTokens[0], authTokens[1]);
                }
            }

            handler.UseProxy = true;
            handler.Proxy = webProxy;
        }

        handler.AutomaticDecompression = DecompressionMethods.All; // добавляет заголовок gzip, deflate, br автоматически и автоматически распаковывает ответы сервера
        handler.AllowAutoRedirect = autoRedirect;
        handler.CookieContainer = cookieContainer ?? new CookieContainer();

        httpClient = new HttpClient(handler);
        httpClient.Timeout = TimeSpan.FromMilliseconds(timeout);
    }

    public Dictionary<string, string> ParseQueryString(string query)
    {
        var collection = HttpUtility.ParseQueryString(query);
        return collection.AllKeys.ToDictionary(k => k, k => collection[k]);
    }

    public async Task<string> RequestAsync(HttpMethod method, string url, Dictionary<string, string> headers, Dictionary<string, string> query = null)
    {
        using var request = new HttpRequestMessage(method, url);

        foreach (var item in headers)
        {
            if (!request.Headers.TryAddWithoutValidation(item.Key, item.Value))
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine($"Не удалось добавить заголовок в запрос ({item.Key} = {item.Value})");
                Console.ResetColor();
            }
        }

        if (method == HttpMethod.Post)
        {
            request.Content = new FormUrlEncodedContent(query);
        }

        using var response = await httpClient.SendAsync(request);
        return await response.Content.ReadAsStringAsync();
    }

    public void Dispose()
    {
        httpClient.Dispose();
    }
}

Использовать как-то так

var httpRequest = new HttpRequest("https://proxy.com:3128", 3000);
var query = httpRequest.ParseQueryString("hello=world&param=123");
string result1 = await httpRequest.RequestAsync(HttpMethod.Post, "example.com", headers, query);
string result2 = await httpRequest.RequestAsync(HttpMethod.Get, "example.net", headers);

Но вообще вы пытаетесь сделать суперуниверсальный метод, который может всё. Из-за этого он становится супер сложным. Сделайте лучше отдельный метод для GET, отдельный для POST, даже можно несколько под разные Content-Type, типа форму, JSON, XML и т.д.

Как разберетесь с этим до конца, вот тогда принимайтесь за IHttpClientFactory и более тонкие настойки SocketsHttpClientHandler.

→ Ссылка