HttpClient requests universally fail unless one is created and used in the Main method of the consuming program

687 views Asked by At

HttpClient requests within class library (.NET Standard 2.0) fail with System.Net.Http.HttpRequestException: An invalid argument was supplied ---> System.Net.Sockets.SocketException: An invalid argument was supplied

Unless a "throw-away" HttpClient is created, used for at least one request, and then disposed (though not really because .NET keeps a static instance) in the Main method (yes it must be the Main method) of the consuming Console App.

I have tried consuming this library via nuget package (we have an internal nuget store), by directly copying the dlls and manually installing all dependencies, and by creating a console app project within the same solution as the library and consuming using project references. All fail in identical ways. (All .NET Core 2.1)

I've tried placing the "throw-away" HttpClient and request in a separate method within the consuming program, as well as several different places within the library, including all the way up in the constructor of the main class in the library.

Here's the "throw-away" HttpClient code:

var testClient = new HttpClient()
{
    BaseAddress = new Uri("https://jsonplaceholder.typicode.com/")
};
await testClient.GetAsync("users");

I have a test suite in the library solution that uses NUnit. This test suite works perfectly. If I run the console app that consumes the library in any way, on Linux (even WSL Ubuntu) it works perfectly.

I can't provide an MRE as the issue is so bizarre and I can't reproduce it readily. However here are snippets in order of initialization leading to the creation of the first HttpClient in the library.

Main Class Constructor

    /// <summary>
    /// OCI DNS API Wrapper
    /// </summary>
    /// <param name="dnsSettings">DNSSettings Object, sets high level configurations for the Wrapper</param>
    /// <param name="logger">NLog Logger</param>
    public DNSAPI(DNSSettings dnsSettings, Logger logger)
    {
         Settings.ApplySettings(dnsSettings);

         APIWebClient.Init(Settings.TENANT_ID, Settings.USER_ID, Settings.FINGERPRINT, Settings.KEY_PATH, Settings.API_VERSION);
        _dnsSettings = dnsSettings;
        Settings.SetLogger(logger, _dnsSettings.LogLevel.Value);
        _errorsTable = new Dictionary<int, DnsError>();
        _keyCount = 1;
    }

APIWebClient Static constructor and Init method

static APIWebClient()
        {
            _httpClient = new HttpClient(new HttpClientHandler()
            {
                AllowAutoRedirect = true,
                PreAuthenticate = true
            })
            {
                BaseAddress = new Uri(Settings.BASE_ADDRESS),
            };

            _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        }

        public static void Init(string tenancyID, string userID, string fingerprint, string pemPath, string version)
        {
            _signer = new Signer(tenancyID, userID, fingerprint, pemPath);
            _version = version;
        }

POST Method and Execute Request Method within APIWebClient

public static async Task<APIResponse> POST(string path, Dictionary<string, string> additionalHeaders, string data = "{}")
        {
            var bytes = Encoding.UTF8.GetBytes(data);

            var request = new HttpRequestMessage(HttpMethod.Post, new Uri($"{Settings.BASE_ADDRESS}/{_version}/{path}"));

            request.Content.Headers.ContentType.MediaType = "application/json";
            request.Headers.Add("x-content-sha256",  Convert.ToBase64String(SHA256.Create().ComputeHash(bytes)));

            if (additionalHeaders != null && additionalHeaders.Count > 0)
                foreach (var pair in additionalHeaders)
                    request.Headers.Add(pair.Key, pair.Value);

            using (var stream = await request.Content.ReadAsStreamAsync())
            {
                stream.Write(bytes, 0, bytes.Length);
            }

            request.Content.Headers.ContentLength = bytes.Length;

            return await ExecuteRequest(request);
        }
private static async Task<APIResponse> ExecuteRequest(HttpRequestMessage request)
        {
            try
            {
                request.Headers.Date = DateTime.UtcNow;
                request.Headers.Host = request.RequestUri.Host;

                _signer.SignRequest(request, request.Method == HttpMethod.Put);

                var webResponse = await _httpClient.SendAsync(request);

                var apiResponse = new APIResponse()
                {
                    Body = await new StreamReader(await webResponse.Content.ReadAsStreamAsync(), true).ReadToEndAsync(),
                    Headers = webResponse.Headers,
                    ResponseCode = webResponse.StatusCode
                };
                return apiResponse;
            }
            catch (WebException e)
            {
                var webResponse = e.Response as HttpWebResponse;
                Settings.Logger.Debug($"Web Exception: {e}");
                Settings.Logger.Debug($"WebException Status: {e.Status}");
                var apiResponse = new APIResponse()
                {
                    Body = await new StreamReader(webResponse.GetResponseStream(), true).ReadToEndAsync(),
                    ResponseCode = webResponse.StatusCode
                };
                webResponse.Dispose();
                return apiResponse;
            }
        }

Sorry for the epic post. I've tried to keep it to minimal information. However, as I am unsure what relevant piece of information in this chain of events could clue us into WTF. I couldn't think of a way to shrink this post down.

EDIT: Here is the full stack trace

System.Net.Http.HttpRequestException: An invalid argument was supplied ---> System.Net.Sockets.SocketException: An invalid argument was supplied
   at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.CreateConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.WaitForCreatedConnectionAsync(ValueTask`1 creationTask)
   at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts)
   at DNSAPI.Lib.APIWebClient.ExecuteRequest(HttpRequestMessage request)

The rest of the Main method creates an instance of a test class that creates the loggeer, initializes DNSAPI and makes a single request. This is all done async.

static async Task Main(string[] args)
        {
            var testClient = new HttpClient()
            {
                BaseAddress = new Uri("https://jsonplaceholder.typicode.com/")
            };
            await testClient.GetAsync("users");

            var wtf = new WTF();

            await wtf.Run();

            Console.ReadLine();
        }

EDIT 2: After attaching MS Symbol server and manually adding break on Exception type System.Net.Http.HttpRequestException. I was taken to a Try/Catch in System.Net.Http.ConnectHelper Looking in the Socket Event Args I found this inner exception

at System.Net.Dns.HostResolutionEndHelper(IAsyncResult asyncResult)
   at System.Net.Dns.EndGetHostAddresses(IAsyncResult asyncResult)
   at System.Net.Sockets.MultipleConnectAsync.DoDnsCallback(IAsyncResult result, Boolean sync)

EDIT 3: Here is the host I am contacting - dns.us-ashburn-1.oraclecloud.com NOTE: This host resolution somehow only fails under the above listed conditions so I am disinclined to believe that it is due to communication error or actual DNS lookup failure.

EDIT 4: I have identified the root source of the failure, though I do not yet know why this is happening. .NET Core when running on Windows uses this NameResolutionPal.Windows.cs from .NET Framework's System.Net Namespace. The HostResolutionBeginHelper method processes the host resolution request in an Async context using AsyncCallback. The steps of this process is handled by an DnsResolveAsyncResult instance. The actual request that fails is NameResolutionPal.GetAddrInfoAsync(asyncResult) This call fails because a prerequisite call asyncResult.StartPostingAsyncOp(false); is somehow being skipped. Why this is occurring I do not yet know. REF: https://source.dot.net/#System.Net.NameResolution/System/Net/DNS.cs,f8023b9c19212708

0

There are 0 answers