HttpClientHandler / HttpClient Memory Leak

后端 未结 4 1404
逝去的感伤
逝去的感伤 2020-12-03 01:24

I have anywhere from 10-150 long living class objects that call methods performing simple HTTPS API calls using HttpClient. Example of a PUT call:



        
相关标签:
4条回答
  • 2020-12-03 02:04

    This is how I change the HttpClientHandler proxy without recreating the object.

    public static void ChangeProxy(this HttpClientHandler handler, WebProxy newProxy)
    {
        if (handler.Proxy is WebProxy currentHandlerProxy)
        {
            currentHandlerProxy.Address = newProxy.Address;
            currentHandlerProxy.Credentials = newProxy.Credentials;
        }
        else
        {
            handler.Proxy = newProxy;
        }
    }
    
    0 讨论(0)
  • 2020-12-03 02:09

    Here is a basic Api Client that uses the HttpClient and HttpClientHandler efficiently. Do NOT recreate HTTPClient for each request. Reuse Httpclient as much as possible

    My Performance Api Client

    using System;
    using System.Net;
    using System.Net.Http;
    using System.Net.Http.Headers;
    using System.Text;
    using System.Threading.Tasks;
    //You need to install package Newtonsoft.Json > https://www.nuget.org/packages/Newtonsoft.Json/
    using Newtonsoft.Json;
    using Newtonsoft.Json.Serialization;
    
    namespace MyApiClient 
    {
        public class MyApiClient : IDisposable
        {
            private readonly TimeSpan _timeout;
            private HttpClient _httpClient;
            private HttpClientHandler _httpClientHandler;
            private readonly string _baseUrl;
            private const string ClientUserAgent = "my-api-client-v1";
            private const string MediaTypeJson = "application/json";
    
            public MyApiClient(string baseUrl, TimeSpan? timeout = null)
            {
                _baseUrl = NormalizeBaseUrl(baseUrl);
                _timeout = timeout ?? TimeSpan.FromSeconds(90);
            }
    
            public async Task<string> PostAsync(string url, object input)
            {
                EnsureHttpClientCreated();
    
                using (var requestContent = new StringContent(ConvertToJsonString(input), Encoding.UTF8, MediaTypeJson))
                {
                    using (var response = await _httpClient.PostAsync(url, requestContent))
                    {
                        response.EnsureSuccessStatusCode();
                        return await response.Content.ReadAsStringAsync();
                    }
                }
            }
    
            public async Task<TResult> PostAsync<TResult>(string url, object input) where TResult : class, new()
            {
                var strResponse = await PostAsync(url, input);
    
                return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
                {
                    ContractResolver = new CamelCasePropertyNamesContractResolver()
                });
            }
    
            public async Task<TResult> GetAsync<TResult>(string url) where TResult : class, new()
            {
                var strResponse = await GetAsync(url);
    
                return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
                {
                    ContractResolver = new CamelCasePropertyNamesContractResolver()
                });
            }
    
            public async Task<string> GetAsync(string url)
            {
                EnsureHttpClientCreated();
    
                using (var response = await _httpClient.GetAsync(url))
                {
                    response.EnsureSuccessStatusCode();
                    return await response.Content.ReadAsStringAsync();
                }
            }
    
            public async Task<string> PutAsync(string url, object input)
            {
                return await PutAsync(url, new StringContent(JsonConvert.SerializeObject(input), Encoding.UTF8, MediaTypeJson));
            }
    
            public async Task<string> PutAsync(string url, HttpContent content)
            {
                EnsureHttpClientCreated();
    
                using (var response = await _httpClient.PutAsync(url, content))
                {
                    response.EnsureSuccessStatusCode();
                    return await response.Content.ReadAsStringAsync();
                }
            }
    
            public async Task<string> DeleteAsync(string url)
            {
                EnsureHttpClientCreated();
    
                using (var response = await _httpClient.DeleteAsync(url))
                {
                    response.EnsureSuccessStatusCode();
                    return await response.Content.ReadAsStringAsync();
                }
            }
    
            public void Dispose()
            {
                _httpClientHandler?.Dispose();
                _httpClient?.Dispose();
            }
    
            private void CreateHttpClient()
            {
                _httpClientHandler = new HttpClientHandler
                {
                    AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip
                };
    
                _httpClient = new HttpClient(_httpClientHandler, false)
                {
                    Timeout = _timeout
                };
    
                _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ClientUserAgent);
    
                if (!string.IsNullOrWhiteSpace(_baseUrl))
                {
                    _httpClient.BaseAddress = new Uri(_baseUrl);
                }
    
                _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeJson));
            }
    
            private void EnsureHttpClientCreated()
            {
                if (_httpClient == null)
                {
                    CreateHttpClient();
                }
            }
    
            private static string ConvertToJsonString(object obj)
            {
                if (obj == null)
                {
                    return string.Empty;
                }
    
                return JsonConvert.SerializeObject(obj, new JsonSerializerSettings
                {
                    ContractResolver = new CamelCasePropertyNamesContractResolver()
                });
            }
    
            private static string NormalizeBaseUrl(string url)
            {
                return url.EndsWith("/") ? url : url + "/";
            }
        }
    }
    

    The usage;

    using ( var client = new MyApiClient("http://localhost:8080"))
    {
        var response = client.GetAsync("api/users/findByUsername?username=alper").Result;
        var userResponse = client.GetAsync<MyUser>("api/users/findByUsername?username=alper").Result;
    }
    

    Note: If you are using a dependency injection library, please register MyApiClient as singleton. It's stateless and safe to reuse the same object for concrete requests.

    0 讨论(0)
  • 2020-12-03 02:10

    Using the repro form Alexandr Nikitin, I was able to discover that this seems to happen ONLY when you have HttpClient be a short lived object. If you make the handler and client long lived this does not seem to happen:

    using System;
    using System.Net.Http;
    using System.Threading.Tasks;
    
    namespace HttpClientMemoryLeak
    {
        using System.Net;
        using System.Threading;
    
        class Program
        {
            static HttpClientHandler handler = new HttpClientHandler();
    
            private static HttpClient client = new HttpClient(handler);
    
            public static async Task TestMethod()
            {
                try
                {
                    using (var response = await client.PutAsync("http://localhost/any/url", null))
                    {
                    }
                }
                catch
                {
                }
            }
    
            static void Main(string[] args)
            {
                for (int i = 0; i < 1000000; i++)
                {
                    Thread.Sleep(10);
                    TestMethod();
                }
    
                Console.WriteLine("Finished!");
                Console.ReadKey();
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-03 02:12

    As Matt Clark mentioned, the default HttpClient leaks when you use it as a short-lived object and create new HttpClients per request.

    As a workaround, I was able to keep using HttpClient as a short-lived object by using the following Nuget package instead of the built-in System.Net.Http assembly: https://www.nuget.org/packages/HttpClient

    Not sure what the origin of this package is, however, as soon as I referenced it the memory leak disappeared. Make sure that you remove the reference to the built-in .NET System.Net.Http library and use the Nuget package instead.

    0 讨论(0)
提交回复
热议问题