C#
HTTP、JSON 与 API 客户端
编写带生命周期管理、超时、取消、响应限制和数据验证的 HttpClient API 客户端。
发布于 2026年7月23日
HTTP、JSON 与 API 客户端
网络调用的失败面远大于本地方法:DNS、连接、TLS、超时、取消、状态码、响应大小、JSON 格式和业务数据都可能失败。可靠客户端必须明确这些层次,并避免错误的生命周期或无上限重试。
一、学习目标
- 通过 IHttpClientFactory 管理 HttpClient
- 构造 URI、请求头和 JSON 内容
- 设置总超时并传播取消
- 区分网络、HTTP、数据和业务错误
- 验证响应类型、大小和内容
- 设计可替换、可离线测试的客户端
二、HttpClient 生命周期
不要每次请求都创建并释放 HttpClient。连接池需要跨请求复用,频繁创建可能耗尽套接字并反复建立 TLS。
注册类型化客户端:
builder.Services.AddHttpClient<RemoteTaskClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.Timeout = TimeSpan.FromSeconds(15);
client.DefaultRequestHeaders.UserAgent.ParseAdd(
"StudyTasks/1.0");
});
public sealed class RemoteTaskClient(
HttpClient httpClient,
ILogger<RemoteTaskClient> logger)
{
}
工厂管理底层处理器生命周期,类型化客户端可以短生命周期创建。不要在请求之间修改共享 DefaultRequestHeaders;每次不同的头放到 HttpRequestMessage。
三、发送 JSON 请求
public async Task<RemoteTaskDto> CreateAsync(
CreateTaskRequest payload,
string token,
CancellationToken cancellationToken)
{
using var request = new HttpRequestMessage(
HttpMethod.Post,
"v1/tasks")
{
Content = JsonContent.Create(payload)
};
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", token);
using HttpResponseMessage response =
await httpClient.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken);
return await ReadTaskAsync(response, cancellationToken);
}
ResponseHeadersRead 在收到响应头后返回,避免先无上限缓冲整个正文;此时必须在读取完成前保持响应对象存活。
Token 来自秘密配置,不进入 URL、日志和异常文本。
四、处理状态码
private static void EnsureExpectedStatus(
HttpResponseMessage response)
{
if (response.StatusCode == HttpStatusCode.NotFound)
{
throw new RemoteTaskNotFoundException();
}
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
throw new RemoteRateLimitException(
response.Headers.RetryAfter);
}
if (!response.IsSuccessStatusCode)
{
throw new RemoteServiceException(
response.StatusCode,
$"远端返回 HTTP {(int)response.StatusCode}");
}
}
EnsureSuccessStatusCode 适合只需通用异常的简单调用;业务客户端通常需要把已知状态码转换成稳定语义。
不要默认把整个错误正文放进异常。它可能巨大、含 HTML、Token 或用户数据。需要诊断时限制字节数并脱敏。
五、验证内容类型和大小
private static async Task<RemoteTaskDto> ReadTaskAsync(
HttpResponseMessage response,
CancellationToken cancellationToken)
{
EnsureExpectedStatus(response);
MediaTypeHeaderValue? contentType =
response.Content.Headers.ContentType;
if (contentType?.MediaType is not "application/json")
{
throw new RemoteProtocolException("响应不是 JSON");
}
long? length = response.Content.Headers.ContentLength;
if (length is > 1_048_576)
{
throw new RemoteProtocolException("响应超过 1 MiB");
}
await using Stream stream =
await response.Content.ReadAsStreamAsync(cancellationToken);
RemoteTaskDto dto =
await JsonSerializer.DeserializeAsync<RemoteTaskDto>(
stream,
JsonOptions,
cancellationToken)
?? throw new RemoteProtocolException("响应根值为 null");
return Validate(dto);
}
缺少 Content-Length 时,流仍可能无限增长。高风险场景用限制读取流或受控缓冲设置硬上限。
JSON 成功反序列化后继续校验 ID、状态、标题和时间关系。
六、错误分类
OperationCanceledException
-> 用户取消或拥有策略的超时
HttpRequestException
-> DNS、连接、TLS、协议传输
非成功状态码
-> 远端明确响应
JsonException / 协议校验
-> 响应格式不符合契约
领域校验失败
-> 格式可读但业务数据非法
只在清楚语义的边界转换异常,并保留 inner exception。CLI 再把类别映射为消息和退出码。
不要把所有问题都报告成“网络错误”;HTTP 401 需要修复凭据,429 可能稍后重试,非法 JSON 则可能是服务版本不兼容。
七、超时与取消
HttpClient.Timeout 提供调用上限,调用方令牌支持用户取消。若某用例需要更短超时:
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken);
timeout.CancelAfter(TimeSpan.FromSeconds(5));
await httpClient.SendAsync(request, timeout.Token);
捕获取消时判断是外部令牌还是内部超时。不要创建令牌源后忘记释放。
超时要覆盖整个有意义的操作,包括读取正文,而不是只覆盖响应头。
八、重试与幂等
可考虑重试的暂时错误:
- 部分连接失败
- HTTP 408、429
- 部分 5xx
重试前必须确认操作幂等。GET 通常可重试;POST 创建任务若没有幂等键,第一次可能已成功但响应丢失,直接重试会创建重复数据。
重试策略应:
- 次数有限
- 指数退避并带随机抖动
- 尊重
Retry-After - 传播取消
- 记录最终失败而非每次都报高等级警报
验证错误、401、403、404 通常不应自动重试。
九、可测试设计
Core 定义:
public interface IRemoteTaskClient
{
Task<IReadOnlyList<StudyTask>> FetchAsync(
CancellationToken cancellationToken);
}
服务测试使用假实现。HTTP 适配器测试则替换 HttpMessageHandler,返回本地构造的响应:
internal sealed class StubHandler(
Func<HttpRequestMessage, HttpResponseMessage> responder)
: HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken) =>
Task.FromResult(responder(request));
}
覆盖成功、401、429、500、错误内容类型、超大正文、非法 JSON、超时和取消。测试不依赖公网。
十、常见错误
每次请求 new HttpClient
会破坏连接复用。使用 IHttpClientFactory 或受控长生命周期客户端。
对所有错误自动重试
会放大故障、重复非幂等操作并延迟明确失败。
只验证 JSON 语法
外部数据必须映射并验证为领域对象。
日志记录完整请求和响应
可能泄露认证、个人数据和大正文。只记录允许的元数据。
十一、练习与自测
练习:
- 实现类型化客户端,设置 BaseAddress、User-Agent 和超时。
- 为五类失败定义稳定异常。
- 使用 StubHandler 覆盖非法 JSON 和取消。
- 为创建任务设计幂等键,再决定哪些错误可重试。
自测:
- 为什么不应每次创建 HttpClient?
ResponseHeadersRead改变了谁的资源责任?- HTTP 500 与非法 JSON 分别属于哪类失败?
- POST 在什么条件下可以安全重试?
十二、官方资料
上一篇:SQLite、事务与数据访问 | 下一篇:异步、并发与取消