.Net Framework HttpClient SendAsync连接及SSL/TLS异常求助
尝试发送POST请求时,调试阶段抛出AggregateException,提示“将内容复制到流时出错”,内部异常为“无法从传输连接读取数据:连接已关闭。将内容复制到流时出错”;未调试时则收到“HttpClientHandler异常 - 请求已中止:无法创建安全SSL/TLS通道...”错误。
已确认:
- 使用
IHttpClientFactory,且已将协议设置为Tls12(符合服务端要求) - Postman携带相同客户端证书可成功发送请求
错误信息
调试时调用堆栈
System.Net.Http.HttpRequestException: 'Fehler beim Kopieren von Inhalt in einen Stream.'
Inner Exception: IOException: Von der Übertragungsverbindung können keine Daten gelesen werden: Die Verbindung wurde geschlossen.
StackTrace: at System.Threading.Tasks.TaskFactory1.FromAsyncCoreLogic(IAsyncResult iar, Func2 endFunction, Action1 endAction, Task1 promise, Boolean requiresSynchronization)
未调试时调用堆栈
12.01.2023 14:25:04:5738|INFO|Service|23192|0| * ITAD-199497 * Exception Caught!
12.01.2023 14:25:04:5749|ERROR|Service|23192|Message :{0} | * ITAD-199497 * Fehler beim Senden der Anforderung.
12.01.2023 14:25:04:5749|INFO|TXMS.PaymentCapture|18768|0| * ITAD-199497 * Caught aggregate exception-Task.Wait behavior
12.01.2023 14:25:04:5749|ERROR|TXMS.PaymentCapture|18768|0| * ITAD-199497 * PayID: 95052fe0a86246569c23976899a20ced. Die Anfrage wurde abgebrochen: Es konnte kein geschützter SSL/TLS-Kanal erstellt werden..
12.01.2023 14:25:04:5749|VERBOSE|TXMS.PaymentCapture|18768|0| * ITAD-199497 * bei System.Net.HttpWebRequest.EndGetRequestStream(IAsyncResult asyncResult, TransportContext& context)
bei System.Net.Http.HttpClientHandler.GetRequestStreamCallback(IAsyncResult ar)
12.01.2023 14:25:04:5758|ERROR|ProcessList|18768|0| * ITAD-199497 * Common.TXMSException: Unhandled exception when calling the API.
网络日志
System.Net.Http Error: 0 : [24128] HttpClient#56105527::SendAsync() - Fehler beim Senden von HttpRequestMessage#10319855. System.Net.Http.HttpRequestException: Fehler beim Kopieren von Inhalt in einen Stream. ---> System.IO.IOException: Von der Übertragungsverbindung können keine Daten gelesen werden: Die Verbindung wurde geschlossen.
bei System.Net.ConnectStream.EndWrite(IAsyncResult asyncResult)
bei System.Threading.Tasks.TaskFactory1.FromAsyncCoreLogic(IAsyncResult iar, Func2 endFunction, Action1 endAction, Task1 promise, Boolean requiresSynchronization)
--- Ende der internen Ausnahmestapelüberwachung ---
bei System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
bei System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
bei Microsoft.Extensions.Http.Logging.LoggingHttpMessageHandler.d__5.MoveNext()
--- Ende der Stapelüberwachung vom vorhergehenden Ort, an dem die Ausnahme ausgelöst wurde ---
bei System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
bei System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
bei Microsoft.Extensions.Http.Logging.LoggingScopeHttpMessageHandler.d__5.MoveNext()
ProcessId=4124
DateTime=2023-01-12T13:40:20.6476208Z
更新后的异常
禁用"仅我的代码"并启用所有"异常设置"后,抛出System.Security.Authentication.AuthenticationException:“收到的消息意外或格式错误”。Wireshark捕获显示已使用TLS1.2和TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256密码套件,符合服务端要求。
相关代码
Payment.cs
private async Task<HttpResponseMessage> CreatePurchase() { PurchaseService purchaseService = new PurchaseService(context, repository, pm.CategoryCode); var httpResponseMessage = await purchaseService.CreatePurchaseRequest(context); return httpResponseMessage; } public async Task<HttpResponseMessage> CreatePurchaseRequest(Context context) { ServicePointManager.Expect100Continue = true; ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; endPoint = repository[$"PurchaseService/Endpoints/CreateSettlementOnlyPurchase"]; // Configure httpClient with IHttpClientFactory HttpClientFactoryProvider httpClientFactoryProvider = new HttpClientFactoryProvider(context.Merchant.Test); // Get client from ServiceCollection var httpClient = httpClientFactoryProvider.GetClient("Purchase"); // Create settlePurchaseRequest object SettlePurchaseRequest settlePurchaseRequest = CreateSettlePurchase(context); // Serialize object into JSON var purchaseRequest = settlePurchaseRequest.ToJson(); // Create digest var payloadDigest = purchaseRequest != null ? Digest(purchaseRequest) : null; Dictionary<string, string> signHeaderInfo = CreateSignHeadersInfo(HeaderDateName, now, "POST", targetURL + endPoint, payloadDigest); // Wrap JSON inside a StringContent object var content = new StringContent(purchaseRequest, Encoding.UTF8, "application/json"); // Post to the endpoint var requestMessage = new HttpRequestMessage(HttpMethod.Post, endPoint); requestMessage.Content = content; requestMessage.Headers.Add(HeaderDateName, now); var guid = Guid.NewGuid().ToString(); requestMessage.Headers.Add("X-Request-ID", guid); var signature = CreateSignature(signHeaderInfo); requestMessage.Headers.Add("Signature", signature); requestMessage.Headers.Add("Digest", payloadDigest); using (HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead)) { // Process response httpResponseMessage.EnsureSuccessStatusCode(); var jsonString = httpResponseMessage.Content.ReadAsStringAsync().Result; if (httpResponseMessage.IsSuccessStatusCode) { return httpResponseMessage; } else { return httpResponseMessage; // Todo: Refactor } } } private IHttpClientFactory HttpClientFactory() { if (_httpClientFactory != null) { return _httpClientFactory; } #region DI Service var serviceCollection = new ServiceCollection(); #region Create Settlement Only Purchase serviceCollection.AddHttpClient("Purchase", client => { client.BaseAddress = new Uri(ApiURL); client.DefaultRequestHeaders.Add("Api-Key", PurchaseApiKey); client.DefaultRequestHeaders.Add("Accept", "*/*"); client.DefaultRequestHeaders.Add("Connection", "Keep-Alive"); client.DefaultRequestHeaders.Add("Keep-Alive", "3600"); client.DefaultRequestHeaders.Add("Host", hostName); }) .ConfigurePrimaryHttpMessageHandler(() => { var handler = new HttpClientHandler(); handler.ClientCertificateOptions = ClientCertificateOption.Manual; handler.ClientCertificates.Add(GetCertificateBySerialNumber()); handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12; //handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; return handler; }); #endregion var serviceProvider = serviceCollection.BuildServiceProvider(); _httpClientFactory = serviceProvider.GetService<IHttpClientFactory>(); return _httpClientFactory; #endregion } public HttpClient GetClient(string clientName) { return HttpClientFactory().CreateClient(clientName); }
分析与修正思路
1. 客户端证书加载问题
- 检查
GetCertificateBySerialNumber()是否正确加载证书的私钥:Postman能成功说明证书本身有效,代码中可能仅加载了公钥证书,未包含私钥。可通过Certificate.HasPrivateKey验证。 - 确认证书存储位置,且程序运行账号拥有读取证书私钥的权限(区分本地机器存储/当前用户存储)。
2. HttpClientFactory的不当使用
- 当前代码每次请求都实例化
HttpClientFactoryProvider并重建ServiceCollection,会创建大量重复HttpClient实例,破坏连接池机制,引发连接异常。 - 修正:在应用启动时将
IHttpClientFactory注册到DI容器(如Startup/Program.cs),通过构造函数注入使用,而非每次请求手动构建。
3. ServicePointManager设置时机
ServicePointManager.SecurityProtocol应在应用启动时设置一次,而非每次请求重复设置,多线程环境下多次设置可能引发冲突。
4. 同步调用异步方法的风险
CreatePurchaseRequest中使用ReadAsStringAsync().Result同步等待异步任务,可能引发死锁或上下文冲突,导致连接异常。改为await httpResponseMessage.Content.ReadAsStringAsync()。
5. 请求头一致性检查
- 对比Postman和代码的所有请求头:
- 移除手动添加的
Host头,HttpClient会自动根据BaseAddress生成,手动设置可能导致格式错误。 - 验证
Digest和Signature的生成逻辑是否与服务端规则匹配(哈希算法、签名格式)。 - 确认
HeaderDateName对应的日期格式符合服务端要求(如RFC 1123格式)。
- 移除手动添加的
6. TLS握手额外配置
- 调试时可临时设置
handler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true跳过证书链验证,确认是否为证书信任问题(生产环境禁用)。 - 尝试设置
ServicePointManager.Expect100Continue = false,部分服务端不支持该特性可能导致握手失败。
内容的提问来源于stack exchange,提问作者Colin Roe

