Skip to content
Bitzsoft.Integrationsbitzsoft.integrations

Concept

HttpClient 使用约定

IHttpClientFactory 使用约定、三种客户端模式、请求审计日志、厂商 SDK 例外、超时与重试策略。

Last updated

HttpClient 是连接器与第三方 API 通信的唯一通道。错误使用 HttpClient(直接 new、Singleton 捕获)会导致 socket 耗尽和 DNS 不刷新。本节约定全库统一的 HttpClient 使用模式、审计日志接入方式,以及超时与重试的处理职责。

核心原则:永远用 IHttpClientFactory

禁止 new HttpClient() 所有 HttpClient 实例必须通过 IHttpClientFactory 获取,由框架管理底层 HttpMessageHandler 的生命周期和连接池复用。

// ✗ 禁止——socket 耗尽 + DNS 不刷新
private static readonly HttpClient _client = new HttpClient();
// ✓ 正确——通过 IHttpClientFactory 获取命名客户端
internal sealed class StripeHttpClient
{
private readonly HttpClient _httpClient;
public StripeHttpClient(IHttpClientFactory httpClientFactory, IStripeConfigProvider config)
{
// ① 按名字匹配注册时配置的客户端
_httpClient = httpClientFactory.CreateClient(config.HttpClientName);
_httpClient.Timeout = options.Timeout;
}
}

三种客户端模式

全库存在三种 HttpClient 接入方式,按使用场景区分。

需要配置超时/默认头且包装在 Internal 类中HttpClient 作为依赖注入框架自动构造一个 Provider 动态选择多个底层客户端

选择 HttpClient 模式

命名客户端
Payment

Typed 客户端
Sms / TeamWork

工厂按名解析
FileStorage

模式一:命名客户端(Payment 推荐)

在 DI 注册时用 AddHttpClient(name) 配置命名客户端,Internal 封装类通过 IHttpClientFactory.CreateClient(name) 获取。Payment 域采用此模式。

// DI 注册——命名客户端 + 审计日志
services.AddHttpClient(options.HttpClientName).AddRequestLogging(options.HttpClientName);
// Internal 封装
internal sealed class StripeHttpClient
{
private readonly HttpClient _httpClient;
public StripeHttpClient(IHttpClientFactory httpClientFactory, IStripeConfigProvider config)
{
_httpClient = httpClientFactory.CreateClient(config.HttpClientName);
_httpClient.Timeout = config.Current.Timeout; // ① 超时从 Options 应用
}
public async Task<JsonElement> PostFormAsync(string url, IDictionary<string, string> form, CancellationToken ct)
{
using var response = await _httpClient.PostAsync(url, new FormUrlEncodedContent(form), ct);
// ② 非 2xx → 抛 PaymentException(在边界转 Result)
if (!response.IsSuccessStatusCode)
throw new PaymentException("Stripe", response.StatusCode.ToString(), ...);
return JsonDocument.Parse(await response.Content.ReadAsStringAsync(ct)).RootElement;
}
}

模式二:Typed 客户端(Sms / TeamWork)

Typed 客户端把 HttpClient 直接注入封装类构造函数,框架自动构造并配置。TeamWork.DingTalk 采用此模式,配合 DelegatingHandler 做令牌注入。

// DI 注册——typed 客户端 + DelegatingHandler + 审计日志
services.AddHttpClient<DingTalkHttpClient>() // ① typed 客户端
.AddRequestLogging<DingTalkHttpClient>() // ② 审计日志
.AddHttpMessageHandler(sp => // ③ DelegatingHandler 注入 access_token
{
var options = sp.GetRequiredService<IOptions<DingTalkOptions>>();
var logger = sp.GetRequiredService<ILogger<DingTalkAuthHandler>>();
var factory = sp.GetRequiredService<IHttpClientFactory>();
return new DingTalkAuthHandler(options, logger, factory);
});
// Internal 封装——HttpClient 由框架注入
internal sealed class DingTalkHttpClient
{
private readonly HttpClient _httpClient;
public DingTalkHttpClient(HttpClient httpClient, ...) // ① 直接注入 HttpClient
{
_httpClient = httpClient;
}
}

模式三:工厂按名解析(FileStorage)

FileStorage 一个 Provider 需要根据文件类型动态选择不同的底层客户端(上传、下载、预签名 URL),用工厂按名解析:

// FileStorage 注册时只注册 HttpClientFactory 能力,不绑定具体客户端
services.AddHttpClient(); // ① 仅注册 IHttpClientFactory,不命名
// Provider 内部按需创建
internal sealed class AwsS3FileStore
{
private readonly IHttpClientFactory _httpClientFactory;
// ② 按场景用不同名字创建客户端
private HttpClient CreateFetchClient() => _httpClientFactory.CreateClient("AwsFetch");
}

请求审计日志

AddRequestLogging 注册

所有 HttpClient 注册时都链式调用 AddRequestLogging(),挂载审计 DelegatingHandler。该 Handler 拦截请求/响应,脱敏后非阻塞写入日志队列。

// 命名客户端审计
services.AddHttpClient(options.HttpClientName)
.AddRequestLogging(options.HttpClientName); // ① 按客户端名记录
// Typed 客户端审计
services.AddHttpClient<DingTalkHttpClient>()
.AddRequestLogging<DingTalkHttpClient>(); // ② 按类型记录

AddRequestLogging 有两个重载:一个接受 string 名字(命名客户端),一个接受泛型类型(typed 客户端)。两者底层都注册 RequestLogHandler 作为 DelegatingHandler。

审计基础设施注册

AddRequestLogging() 同时幂等注册审计基础设施(IRequestLogRecorder、日志存储)。重复调用不会重复注册:

// 幂等——多个 Provider 调用时只注册一次基础设施
services.AddRequestLogging(); // 在 Sms 等历史域中显式调用

厂商 SDK 例外

部分厂商(阿里云短信、腾讯云短信)使用自带 HTTP 管道而非 IHttpClientFactory。这些 SDK 的请求无法通过 DelegatingHandler 拦截,审计日志改用 IRequestLogRecorder.Record() 回调写入。

// Sms 域——阿里云 SDK 自带 HTTP 管道
public static IServiceCollection AddAliyunSms(
this IServiceCollection services, Action<AliyunSmsOptions> configure)
{
services.Configure(configure);
// ① SDK 自带管道,无法用 DelegatingHandler,但仍注册审计基础设施
services.AddRequestLogging();
// ② 审计通过 IRequestLogRecorder.Record() 回调在 SDK 调用处手动写入
services.AddTransient<ISMS, AliyunSmsService>();
return services;
}

超时约定

全库没有统一的 HttpClient 全局超时。每个域在 Options 中定义自己的超时字段,在构造 HttpClient 封装时应用。

超时字段类型默认值
PaymentStripeOptions.TimeoutTimeSpan30 秒
TeamWorkDingTalkOptions 内部定义
BeisenBeisenOptions.TimeoutMsint(毫秒)30000(30 秒)
// Payment 应用超时
public StripeHttpClient(IHttpClientFactory httpClientFactory, IStripeConfigProvider config)
{
_httpClient = httpClientFactory.CreateClient(config.HttpClientName);
_httpClient.Timeout = config.Current.Timeout; // ① 从 Options 应用
}

重试约定

全库不内置 Polly,不提供自动重试。第三方 API 失败的重试策略由消费者负责。

失败瞬时错误永久错误

Provider 调用第三方 API

返回 Result.Fail
带 ErrorCode

消费者决定

消费者实现重试
指数退避 + 幂等键

放弃 / 告警

设计理由

  1. 幂等性是消费者职责:支付退款等操作的重试必须配合幂等键,库无法判断某次失败是否可安全重试。
  2. IntegrationException.IsTransient 提供判断依据:消费者可据此区分瞬时错误(网络、限流)和永久错误(认证失败、参数错误)。
  3. 避免隐藏的重复调用:自动重试可能导致意外的重复扣款或重复发送,显式重试更安全。

CancellationToken 约定

所有 HTTP 方法必须接受并传递 CancellationToken,绝不吞掉取消:

public async Task<JsonElement> PostFormAsync(
string url, IDictionary<string, string> form, CancellationToken ct)
{
// ① ct 一直传到底层 HttpClient
using var response = await _httpClient.PostAsync(url, new FormUrlEncodedContent(form), ct);
// ...
}

取消是控制流语义,不是失败。详见 错误处理约定

相关约定

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%