C# HttpClient 连接被远程主机强制关闭
结论
这个错误通常与 JSON、Basic Authentication 或 HttpClient 无关,更可能是 TLS 协议版本不匹配。
.NET Framework 4.5 建立 HTTPS 连接时,默认协议可能是 TLS 1.0。如果支付接口只接受 TLS 1.2,服务器会在 TLS 握手阶段断开连接,客户端随即报错:
An existing connection was forcibly closed by the remote host
请在创建或首次使用 HttpClient 前启用 TLS 1.2:
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
推荐修改
建议在应用启动时统一设置。例如,在 ASP.NET MVC 的 Global.asax 中配置:
using System.Net;
using System.Web;
public class MvcApplication : HttpApplication
{
protected void Application_Start()
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
// 其他 MVC 初始化代码
}
}
也可以在客户端构造函数中设置,不过放在应用启动阶段更容易管理:
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
public class AlternativePaymentsCli
{
private readonly string _apiSecretKey;
private readonly HttpClient _httpClient;
public AlternativePaymentsCli(string apiSecretKey)
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
_apiSecretKey = apiSecretKey;
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
var credentials = Encoding.ASCII.GetBytes(apiSecretKey + ":");
var authInfo = Convert.ToBase64String(credentials);
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", authInfo);
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(
"Anything.com-custom-client/1.0");
}
}
建议删除下面两个手动添加的请求头:
_httpClient.DefaultRequestHeaders.Add("Connection", "Keep-Alive");
_httpClient.DefaultRequestHeaders.Add("Keep-Alive", "3600");
HttpClient 会自行管理持久连接。这两个请求头无法解决 TLS 握手失败,部分代理或服务器也可能不接受自定义的 Keep-Alive 请求头。
改用异步调用
TLS 配置是这次异常的主要排查方向。此外,在 ASP.NET 中应尽量避免使用 .Result 和 GetAwaiter().GetResult()。同步等待异步操作会阻塞线程,在某些上下文中还可能导致死锁。
可以将整条调用链改为异步:
using Newtonsoft.Json;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
public async Task<string> CreateHostedPageTransactionAsync(
HostedPageRequest req)
{
var settings = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
};
var json = JsonConvert.SerializeObject(req, settings);
using (var content = new StringContent(
json,
Encoding.UTF8,
"application/json"))
{
using (var response = await _httpClient.PostAsync(
_baseUrl + "/transactions/hosted",
content))
{
var responseText =
await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(
string.Format(
"Alternative Payments returned HTTP {0} ({1}). Response: {2}",
(int)response.StatusCode,
response.ReasonPhrase,
responseText));
}
return responseText;
}
}
}
请求失败时不要只返回空字符串,否则服务器返回的状态码和响应正文都会丢失,之后很难分辨问题出在认证、请求参数还是服务端。
为什么 Postman 和 cURL 可以正常请求
Postman 和较新的 cURL 通常能自动协商 TLS 1.2 或更高版本。旧版 .NET Framework 应用使用的默认协议可能不同,所以即使 URL、JSON 和认证信息完全一致,连接结果仍可能不同。
异常出现在:
System.Net.TlsStream.EndWrite
这表明连接很可能在 HTTPS/TLS 层就已终止,请求还没有进入正常的 HTTP 响应阶段。
如果启用 TLS 1.2 后仍然失败
可以继续检查这些项目:
- 确认应用实际运行的 .NET Framework 版本和目标框架,不要只看开发机器安装了哪个版本。
- 确认服务器操作系统支持 TLS 1.2,并且启用了兼容的加密套件。
- 检查公司代理、防火墙、杀毒软件或 TLS 检查设备是否重置了连接。
- 检查
_baseUrl是否指向正确的 HTTPS 地址,以及测试环境和生产环境是否使用了不同域名。 - 确认运行应用的服务器信任接口所使用的证书链。
- 查看完整的
InnerException。TLS、证书和代理问题通常会在其中留下更具体的信息。
不要关闭证书验证来规避问题,尤其是在对接支付接口时。如果设置 SecurityProtocolType.Tls12 后仍然无效,应升级 .NET Framework、安装操作系统补丁,并向接口方核实目前要求的 TLS 版本和加密套件。
备注:内容仅供参考。
