25

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:

using (HttpClientHandler handler = new HttpClientHandler())
{
    handler.UseCookies = true;
    handler.CookieContainer = _Cookies;

    using (HttpClient client = new HttpClient(handler, true))
    {
        client.Timeout = new TimeSpan(0, 0, (int)(SettingsData.Values.ProxyTimeout * 1.5));
        client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Statics.UserAgent);

        try
        {
            using (StringContent sData = new StringContent(data, Encoding.UTF8, contentType))
            using (HttpResponseMessage response = await client.PutAsync(url, sData))
            {
                using (var content = response.Content)
                {
                    ret = await content.ReadAsStringAsync();
                }

            }
        }
        catch (ThreadAbortException)
        {
            throw;
        }
        catch (Exception ex)
        {
            LastErrorText = ex.Message;
        }
    }
}

After 2-3 hours of running these methods, which include proper disposal via using statements, the program has creeped to 1GB-1.5GB of memory and eventually crashes with various out of memory errors. Many times the connections are through unreliable proxies, so the connections may not complete as expected (timeouts and other errors are common).

.NET Memory Profiler has indicated that HttpClientHandler is the main issue here, stating it has both 'Disposed instances with direct delegate roots' (red exclamation mark) and 'Instances that have been disposed but are still not GCed' (yellow exclamation mark). The delegates that the profiler indicates have been rooted are AsyncCallbacks, stemming from HttpWebRequest.

It may also relate to RemoteCertValidationCallback, something to do with HTTPS cert validation, as the TlsStream is an object further down in the root that is 'Disposed but not GCed'.

With all this in mind - how can I more correctly use HttpClient and avoid these memory issues? Should I force a GC.Collect() every hour or so? I know that is considered bad practice but I don't know how else to reclaim this memory that isn't quite properly being disposed of, and a better usage pattern for these short-lived objects isn't apparent to me as it seems to be a flaw in the .NET objects themselves.


UPDATE Forcing GC.Collect() had no effect.

Total managed bytes for the process remain consistent around 20-30 MB at most while the process overall memory (in Task Manager) continues to climb, indicating an unmanaged memory leak. Thus this usage pattern is creating an unmanaged memory leak.

I have tried creating class level instances of both HttpClient and HttpClientHandler per the suggestion, but this has had no appreciable effect. Even when I set these to class level, they are still re-created and seldom re-used due to the fact that the proxy settings often require changing. HttpClientHandler does not allow modification of proxy settings or any properties once a request has been initiated, so I am constantly re-creating the handler, just as was originally done with the independent using statements.

HttpClienthandler is still being disposed with "direct delegate roots" to AsyncCallback -> HttpWebRequest. I'm starting to wonder if maybe the HttpClient just wasn't designed for fast requests and short-living objects. No end in sight.. hoping someone has a suggestion to make the use of HttpClientHandler viable.


Memory profiler shots: Initial stack indicating that HttpClientHandler is the root issue, having 304 live instances that should have been GC'd

enter image description here

enter image description here

user1111380
  • 551
  • 2
  • 6
  • 17
  • Why are you disposing `HttpClientHandler` and `HttpClient` on each call? `HttpClient` should be a long lived object throughout your application (and accordingly, so should `HttpClientHandler` . That way, you only need to generate one instance. – Yuval Itzchakov Jan 01 '15 at 16:37
  • I snipped out unimportant parts of the code but the headers fluctuate depending on the request, and the proxies change based on their success/lack of success with the connection. A lot of maintenance on the HttpClientHandler object would be occurring so I thought it would be simpler to just recreate it each time. However it still doesn't satisfy the question of why these objects cannot be re-created repeatedly without leaking – user1111380 Jan 01 '15 at 16:39
  • If you do try the GC.collect(), do you see these TlsStream objects being collected? – zaitsman Jan 02 '15 at 09:33
  • @zaitsman No effect on the TlsStream objects. – user1111380 Jan 03 '15 at 01:39
  • 2
    It's hard to see what's going on, but generally: find out the roots. Enable .net source code debugging and understand when those roots are supposed to be freed, and why? Try removing `handler.CookieContainer = _CookieContainer` - maybe there's something fishy going on with that? – Erti-Chris Eelmaa Jan 03 '15 at 17:13
  • 2
    I'm not certain but if I recall correctly using() is not recommended with general http because when you have an error it does not free al the memory and you need to call abort on the connection. – Pedro.The.Kid Jan 05 '15 at 12:46
  • @Pedro.The.Kid even after reverting to a class-wide object that was only disposed of as necessary (which was frequently because of my particular usage pattern; proxies tend to be unreliable) it had no effect. – user1111380 Jan 06 '15 at 14:42
  • @ChrisEelmaa I've moved on to just using HttpWebRequest async at this point, which the HttpClient is just a wrapper for, and it has alleviated the memory issues two-fold (e.g. before the application crashed after 3 hours, now it will run for 8). I don't see a solution to this and it appears to be a flaw in the framework, as you suggested - spending more time trying to resolve it is beyond my interest level at this point. Thanks for the suggestions. – user1111380 Jan 06 '15 at 14:44
  • @user1111380 HttpClientHandler must have something connected to it, see if the response connection is being closed? Not being able to GC an object is a major issue that would be piked up very fast, the issue must be on your code. – Pedro.The.Kid Jan 06 '15 at 16:27
  • try to add this as your first catch "catch (WebException ex) { ex.Response.Close(); }" – Pedro.The.Kid Jan 06 '15 at 16:32
  • Try to lower down your timeout see if the connection retention is the problem. – Kuqd Jan 06 '15 at 16:38
  • 1
    Quite interesting case. Concise repro https://gist.github.com/alexandrnikitin/6b2e71c27ce5e9ec5601 – Alexandr Nikitin Jan 06 '15 at 20:58
  • @AlexandrNikitin; yep, leaking here. – Erti-Chris Eelmaa Jan 06 '15 at 22:12
  • 1
    @ChrisEelmaa I used void instead of Task in prev repro, my fault :) Actually it doesn't leak, here's my repro https://gist.github.com/alexandrnikitin/86b3e5a517455f7ff8b0 – Alexandr Nikitin Jan 07 '15 at 00:00
  • @AlexandrNikitin nice efforts to recreate, thank you. I think the only variable unaccounted for is that when connecting to remote proxies, as stated above the connection is often unreliable, leading to a myriad of WebExceptions etc., and the entire using block is thus enclosed in a try/catch. I'm not sure if it's possible to accurately re-create these conditions in a test -- perhaps just throwing random exceptions could do it, but throwing an exception at the application level may behave differently than when the socket receives the actual exception mid-connnection, which could be related to – user1111380 Jan 07 '15 at 17:29
  • the problem (ran out of characters in last comment) – user1111380 Jan 07 '15 at 17:29
  • Do you have stacktraces of those exceptions? – Alexandr Nikitin Jan 07 '15 at 21:11
  • @AlexandrNikitin I know it's been a while but is this still an issue in newer versions of .NET? Your code doesn't leak for me and I can't find any information on this anywhere... – user1935361 Feb 01 '19 at 18:23
  • @user1935361 actually I don't know that. The second repro I posted doesn't leak either. – Alexandr Nikitin Feb 02 '19 at 02:40

4 Answers4

17

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();
        }
    }
}
John Saunders
  • 160,644
  • 26
  • 247
  • 397
Matt Clark
  • 1,171
  • 6
  • 12
  • 4
    Thanks for getting to the bottom of this. Unfortunately the HttpClient class does not meet my requirements then - due to the dynamic and unstable nature of public proxies, the objects HAVE to be often re-created. It appears HttpClient is just not a feasible solution for short-living connections -- changing the proxy settings requires re-constructing the HttpClientHandler, and thus the HttpClient. Either way, the objects should be able to live as long or short as needed without leaking; this definitely seems to be a flaw in the HttpClient. – user1111380 Jan 09 '15 at 03:27
3

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.

Alper Ebicoglu
  • 8,884
  • 1
  • 49
  • 55
1

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;
    }
}
Eray Balkanli
  • 7,752
  • 11
  • 48
  • 82
chancity
  • 31
  • 2
-1

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.

Elad Nava
  • 7,746
  • 2
  • 41
  • 61
  • unfortunately it seems the owner has unlisted this package "The owner has unlisted this package. This could mean that the package is deprecated or shouldn't be used anymore." – Choco Smith Sep 01 '16 at 00:52
  • You can still use it even though it's unlisted. It still works. – Elad Nava Sep 01 '16 at 09:29