runtime: APNs + HTTP/2 + HttpClient + ClientCertificate throws WinHttpException

I’m trying to sent message to APNs using HttpClient and p12 certificate.

Unfortunatelly I’m getting The server returned an invalid or unrecognized response error on SendAsync.

Here is Code Example:

public async Task SendAsync(ApnsHttp2Notification notification)
{
    var url = string.Format("https://{0}:{1}/3/device/{2}",
      _options.Host,
      _options.Port,
      notification.DeviceToken);

    var uri = new Uri(url);

    using (var certificate = SecurityHelperMethods.GetCertificateFromFile(_options.CertificateFileName, _options.CertificatePassword))
    using (var httpHandler = new HttpClientHandler { SslProtocols = SslProtocols.Tls12 })
    {
        httpHandler.ClientCertificates.Add(certificate);
        using (var httpClient = new HttpClient(httpHandler, true))
        using (var request = new HttpRequestMessage(HttpMethod.Post, url))
        {
            request.Content = new StringContent("Test");
            request.Version = new Version(2, 0);
            try
            {
                using (var httpResponseMessage = await httpClient.SendAsync(request))
                {
                    var responseContent = await httpResponseMessage.Content.ReadAsStringAsync();
                    var result = $"status: {(int)httpResponseMessage.StatusCode} content: {responseContent}";
                }
            }
            catch (Exception e)
            {
                throw;
            }
        }
    }
}

Exception:

{System.Net.Http.HttpRequestException: Error while copying content to a stream. ---> System.IO.IOException: The write operation failed, see inner exception. ---> System.Net.Http.WinHttpException: The server returned an invalid or unrecognized response
   --- End of inner exception stack trace ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Net.Http.HttpContent.<CopyToAsyncCore>d__44.MoveNext()
   --- End of inner exception stack trace ---
   at System.Net.Http.HttpContent.<CopyToAsyncCore>d__44.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Net.Http.WinHttpHandler.<InternalSendRequestBodyAsync>d__131.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Net.Http.WinHttpHandler.<StartRequest>d__105.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
   at System.Net.Http.HttpClient.<FinishSendAsyncBuffered>d__58.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at APNs.ApnService.<SendAsync>d__4.MoveNext() in D:\projects\ipl\src\Services\Notifiers\IPL.APNsNotifier.Worker\APNs\ApnService.cs:line 69}
    Data: {System.Collections.ListDictionaryInternal}
    HResult: -2146232800
    HelpLink: null
    InnerException: {System.IO.IOException: The write operation failed, see inner exception. ---> System.Net.Http.WinHttpException: The server returned an invalid or unrecognized response
   --- End of inner exception stack trace ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Net.Http.HttpContent.<CopyToAsyncCore>d__44.MoveNext()}
    Message: "Error while copying content to a stream."
    Source: "System.Net.Http"
    StackTrace: "   at System.Net.Http.HttpContent.<CopyToAsyncCore>d__44.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at System.Net.Http.WinHttpHandler.<InternalSendRequestBodyAsync>d__131.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at System.Net.Http.WinHttpHandler.<StartRequest>d__105.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at Sys
tem.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()\r\n   at System.Net.Http.HttpClient.<FinishSendAsyncBuffered>d__58.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()\r\n   at APNs.ApnService.<SendAsync>d__4.MoveNext() in ...\\ApnService.cs:line 69"
    TargetSite: {Void MoveNext()}

Target Framework: netcoreapp2.0 OS: Windows 10 OS Version: 1703 OS Build: 15063.332

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 6
  • Comments: 32 (8 by maintainers)

Most upvoted comments

Yes @cgyan009 this is the code i use. If using .NET Framework you have to get the System.Net.Http.WinHttpHandler (4.4x) nuget package (.NET Core supports HTTP2)


public class CustomHttpHandler : WinHttpHandler { }

public class ApnsProvider : IDisposable
{
    HttpClient _client;
    CustomHttpHandler _handler;

    private readonly string _appBundleId;
    private readonly string _apnBaseUrl;

    public ApnsProvider(string apnUrl, string appBundleId)
    {
        _appBundleId = appBundleId;
        _apnBaseUrl = apnUrl;
        _handler = new CustomHttpHandler();
        _client = new HttpClient(_handler);
    }

    public async Task<bool> SendAsync(string message, string deviceToken, string jwtToken, bool playSound, string debugInfo = null)
    {
        var success = false;
        var headers = GetHeaders();

        var obj = new
        {
            aps = new
            {
                alert = message,
                sound = playSound ? "default" : null
            }
        };

        var json = Newtonsoft.Json.JsonConvert.SerializeObject(obj);
        var data = new StringContent(json);

        using (var request = new HttpRequestMessage(HttpMethod.Post, _apnBaseUrl + "/3/device/" + deviceToken))
        {
            request.Version = new Version(2, 0);
            request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", jwtToken);
            request.Content = data;

            foreach (var header in headers)
            {
                request.Headers.Add(header.Key, header.Value);
            }

            using (var response = await _client.SendAsync(request))
            {
                success = response.IsSuccessStatusCode;
            }
        }

        return success;
    }

    private Dictionary<string, string> GetHeaders()
    {
        var messageGuid = Guid.NewGuid().ToString();
        var headers = new Dictionary<string, string>();

        headers.Add("apns-id", messageGuid);
        headers.Add("apns-expiration", "0");
        headers.Add("apns-priority", "10");
        headers.Add("apns-topic", _appBundleId);

        return headers;
    }

    public void Dispose()
    {
        _handler.Dispose();
        _client.Dispose();
    }
}

Is there a possibility that ‘tracking-external-issue’ label not really correct? Considering that issue is reproducible with SocketsHttpHandler but works with CurlHandler (when enabled with environment variable in NET Core 2.1 on linux)

This issue is tracking only WinHttpHandler. So, the ‘tracking-external-issue’ label is appropriate.

This issue is not tracking SocketsHttpHandler work to enable this scenario. SocketsHttpHandler currently doesn’t support HTTP/2.0. So, the scenario is not even possible in SocketsHttpHandler. A future version of .NET Core will update SocketsHttpHandler to support HTTP/2.0.

Maybe a slightly improved and 100% working APN wrapper:

    public class ApnHttp2Sender : IDisposable
    {
        private static readonly Dictionary<ApnServerType, string> servers = new Dictionary<ApnServerType, string>
        {
            {ApnServerType.Development, "https://api.development.push.apple.com:443" },
            {ApnServerType.Production, "https://api.push.apple.com:443" }
        };

        private const string apnidHeader = "apns-id";

        private readonly string p8privateKey;
        private readonly string p8privateKeyId;
        private readonly string teamId;
        private readonly string appBundleIdentifier;
        private readonly ApnServerType server;
        private readonly Lazy<string> jwtToken;
        private readonly Lazy<HttpClient> http;
        private readonly Lazy<Http2Handler> handler;

        /// <summary>
        /// Initialize sender
        /// </summary>
        /// <param name="p8privateKey">p8 certificate string</param>
        /// <param name="privateKeyId">10 digit p8 certificate id. Usually a part of a downloadable certificate filename</param>
        /// <param name="teamId">Apple 10 digit team id</param>
        /// <param name="appBundleIdentifier">App slug / bundle name</param>
        /// <param name="server">Development or Production server</param>
        public ApnHttp2Sender(string p8privateKey, string p8privateKeyId, string teamId, string appBundleIdentifier, ApnServerType server)
        {
            this.p8privateKey = p8privateKey;
            this.p8privateKeyId = p8privateKeyId;
            this.teamId = teamId;
            this.server = server;
            this.appBundleIdentifier = appBundleIdentifier;
            this.jwtToken = new Lazy<string>(() => CreateJwtToken());
            this.handler = new Lazy<Http2Handler>(() => new Http2Handler());
            this.http = new Lazy<HttpClient>(() => new HttpClient(handler.Value));
        }

        public async Task<ApnSendResult> SendAsync<TNotification>(
            string deviceToken,
            TNotification notification,
            string apnsId = null,
            string apnsExpiration = "0",
            string apnsPriority = "10")
        {
            var path = $"/3/device/{deviceToken}";
            var json = JsonConvert.SerializeObject(notification);
            var result = new ApnSendResult { NotificationId = apnsId };
            var headers = new NameValueCollection
            {
                { ":method", "POST" },
                { ":path", path },
                { "authorization", $"bearer {jwtToken.Value}" },
                { "apns-topic", appBundleIdentifier },
                { "apns-expiration", apnsExpiration },
                { "apns-priority", apnsPriority }
            };

            if (!string.IsNullOrWhiteSpace(apnsId))
            {
                headers.Add(apnidHeader, apnsId);
            }

            try
            {
                Retry:
                var request = new HttpRequestMessage(HttpMethod.Post, new Uri(servers[server] + path))
                {
                    Version = new Version(2, 0),
                    Content = new StringContent(json)
                };
                request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", jwtToken.Value);
                request.Headers.TryAddWithoutValidation(":method", "POST");
                request.Headers.TryAddWithoutValidation(":path", path);
                request.Headers.Add("apns-topic", appBundleIdentifier);
                request.Headers.Add("apns-expiration", apnsExpiration);
                request.Headers.Add("apns-priority", apnsPriority);

                var response = await http.Value.SendAsync(request);
                if (response.StatusCode == HttpStatusCode.TooManyRequests)
                {
                    Console.WriteLine("Retrying in a second");
                    await Task.Delay(1000);
                    goto Retry;
                }

                result.Success = response.IsSuccessStatusCode;
                result.Status = response.StatusCode;
                result.NotificationId = response.Headers.GetValues(apnidHeader).FirstOrDefault();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
                result.Exception = ex;
            }

            return result;
        }

        public void Dispose()
        {
            if (http.IsValueCreated)
            {
                handler.Value.Dispose();
                http.Value.Dispose();
            }
        }

        private string CreateJwtToken()
        {
            var header = JsonConvert.SerializeObject(new { alg = "ES256", kid = p8privateKeyId });
            var payload = JsonConvert.SerializeObject(new { iss = teamId, iat = ToEpoch(DateTime.UtcNow) });

            var key = CngKey.Import(Convert.FromBase64String(p8privateKey), CngKeyBlobFormat.Pkcs8PrivateBlob);
            using (var dsa = new ECDsaCng(key))
            {
                dsa.HashAlgorithm = CngAlgorithm.Sha256;
                var headerBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(header));
                var payloadBasae64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(payload));
                var unsignedJwtData = $"{headerBase64}.{payloadBasae64}";
                var signature = dsa.SignData(Encoding.UTF8.GetBytes(unsignedJwtData));
                return $"{unsignedJwtData}.{Convert.ToBase64String(signature)}";
            }
        }

        private static int ToEpoch(DateTime time)
        {
            var span = DateTime.UtcNow - new DateTime(1970, 1, 1);
            return Convert.ToInt32(span.TotalSeconds);
        }

        private class Http2Handler : WinHttpHandler { }
    }

    public class ApnSendResult
    {
        public bool Success { get; set; }
        public HttpStatusCode Status { get; set; }
        public string NotificationId { get; set; }
        public Exception Exception { get; set; }

        public override string ToString()
        {
            return $"{Success}, {Status}, {Exception?.Message}";
        }
    }

    public enum ApnServerType
    {
        Development,
        Production
    }