Zeek.Http 10.0.25
Zeek.Http
Zeek.Http 是面向 Zeek 桌面应用的薄 HTTP client 基础设施模块。
它基于 .NET IHttpClientFactory,提供 endpoint 配置、named/typed HttpClient 注册、显式通用 header、显式授权 handler、HTTP 错误分类、AOT 友好的 JSON helper 和基础上传下载 helper。
它不是 REST client 平台,不绑定 Refit,不提供服务端 Host,不生成接口代理,也不接管 token 生命周期。
安装
发布包消费时引用:
<ItemGroup>
<PackageReference Include="Zeek.Http" />
</ItemGroup>
当前仓库源码 sample 在模块未发布阶段使用本地 ProjectReference;正式业务项目应使用发布包。
源码结构
模块源码按能力边界分目录,但 public API namespace 不变:
Building/ # ModuleContext / IServiceCollection 注册入口
Configuration/ # endpoint options
Authorization/ # 授权 provider、通用 header provider、匿名 request option
Errors/ # 错误分类、错误模型和异常
Json/ # 显式 JsonTypeInfo<T> helper
Transfer/ # 基础 stream 上传下载
Internal/ # handler、options reader、默认实现
引入模块
业务模块通过依赖 HttpModule 接入:
using Zeek.Core;
using Zeek.Http;
public sealed class AppModule : IZeekModule
{
public static IReadOnlyList<ModuleDependency> Dependencies =>
[ModuleDependency.Of<HttpModule>()];
}
HttpModule 只在 Build 阶段注册服务,不会在启动阶段访问网络。
配置 endpoint
配置文件只描述 HTTP endpoint 的 BaseAddress 和 Timeout:
{
"Http": {
"Clients": {
"Default": {
"BaseAddress": "https://api.example.com/",
"Timeout": "00:00:30"
},
"ReportApi": {
"BaseAddress": "https://report.example.com/",
"Timeout": "00:01:00"
}
}
}
}
规则:
Default是默认 endpoint 名称。- named endpoint 不继承
Default。 - 每个 endpoint 都必须配置自己的
BaseAddress。 - token、secret、api key、授权开关不放入
Http:Clients。 - 是否添加授权由注册代码显式表达。
注册 HttpClient
默认 endpoint:
context.AddZeekHttpClient();
named endpoint:
context.AddZeekHttpClient("ReportApi");
typed client:
context.AddZeekHttpClient<IOrderApi, OrderApiClient>()
.AddZeekHttpHeaders(ZeekHttpOptions.DefaultEndpointName)
.AddZeekHttpAuthorization<OrderAuthorizationProvider>(ZeekHttpOptions.DefaultEndpointName);
AddZeekHttpClient(...) 返回原生 IHttpClientBuilder,可以继续接入 .NET handler、test handler、logging handler 或 resilience 扩展。
通用 header
实现 IZeekHttpHeaderProvider:
public sealed class AppHeaderProvider : IZeekHttpHeaderProvider
{
public ValueTask ApplyAsync(
HttpRequestMessage request,
CancellationToken cancellationToken = default)
{
request.Headers.TryAddWithoutValidation("X-App-Client", "desktop");
return ValueTask.CompletedTask;
}
}
注册并显式接入:
context.Services.AddSingleton<IZeekHttpHeaderProvider, AppHeaderProvider>();
context.AddZeekHttpClient()
.AddZeekHttpHeaders(ZeekHttpOptions.DefaultEndpointName);
未调用 AddZeekHttpHeaders(...) 时,即使存在 provider,也不会自动添加 header。
授权
实现 IZeekHttpAuthorizationProvider:
public sealed class GtrAuthProvider(GtrAuthService authService)
: IZeekHttpAuthorizationProvider
{
public async ValueTask ApplyAsync(
HttpRequestMessage request,
CancellationToken cancellationToken = default)
{
string? accessToken = await authService.EnsureAccessTokenAsync(cancellationToken);
if (!string.IsNullOrWhiteSpace(accessToken))
{
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
}
}
}
应用应注册具体授权 provider,并给 API client 显式绑定 provider 类型:
context.Services.AddSingleton<GtrAuthService>();
context.Services.AddTransient<GtrAuthProvider>();
context.AddZeekHttpApi<GtrAuthProvider>(api => api
.Add<IOrderApi, OrderApiClient>()
.Add<ICustomerApi, CustomerApiClient>()
.Add<IProductApi, ProductApiClient>());
TAuthorizationProvider 必须是具体 provider 类型;接口或抽象类型会在注册阶段失败。
多服务端应用应为不同系统保留独立 provider 类型,并在 client 上直接绑定:
context.Services.AddSingleton<OrderAuthorizationProvider>();
context.Services.AddSingleton<ReportAuthorizationProvider>();
context.AddZeekHttpApi<OrderAuthorizationProvider>("OrderApi", api => api
.Add<IOrderApi, OrderApiClient>()
.Add<ICustomerApi, CustomerApiClient>());
context.AddZeekHttpApi<ReportAuthorizationProvider>("ReportApi", api => api
.Add<IReportApi, ReportApiClient>());
每个 provider 独立处理自己的 Bearer token、API key、OAuth token 或签名;不要把多个系统的鉴权集中到一个 provider 里按 endpoint switch。Zeek.Http 不内置 JWT、OAuth 或 API Key scheme DSL。
登录等少数匿名请求可在 HttpRequestMessage.Options 中设置 ZeekHttpRequestOptions.AllowAnonymous,以跳过当前 client 绑定的 provider。
权限验证闭环
Zeek.Http 的“授权”只发生在客户端请求发送前:provider 给请求写入 Authorization、签名、租户或业务 header。真正的账号是否有效、角色是否允许、数据权限是否通过,仍由服务端返回 200、401、403 或业务错误码来表达。
这里的边界很重要:
AllowAnonymous只跳过客户端授权 provider,不代表服务端允许匿名访问。IZeekHttpAuthorizationProvider不应该弹窗、不应该跳转登录页,也不应该解释业务错误码。- 401 表示服务端拒绝当前凭据;可以由应用自定义 handler 做 forced refresh + retry once。
- 403 表示服务端识别了身份但权限不足;通常不应该自动 refresh 重试。
- GTR 的 Bearer/JWT、ERP 的自定义 header、MES 的签名请求都应放在各自后端模块的 provider / AuthService 中。
授权更新和 JWT refresh
Zeek.Http / Zeek.Http.Refit 不把 JWT refresh 做成框架内置功能。推荐边界是:
IZeekHttpAuthorizationProvider只负责在请求发送前确保 access token 可用,并写入Authorizationheader。- 应用 Auth 模块负责 bearer token、ERP header credential、refresh token rotation、过期时间懒刷新、single-flight 并发合并、退出登录和 UI 跳转。
- 登录和 refresh API 使用整体匿名 client,例如
AddZeekRefitAnonymousClient<IGtrAuthApi>(...)。 - 业务 API 使用应用封装后的
AddGtrApis(...)、AddErpApis(...),内部再调用AddZeekRefitApi<TAuthorizationProvider>(...)或AddZeekHttpApi<TAuthorizationProvider>(...)。 - 401 后 forced refresh + retry 一次应放在应用自定义
DelegatingHandler,通过返回的IHttpClientBuilder追加。 { code, message, data }或{ success, msg, result }这类 response envelope 解包属于应用 API 模块;可以按文档示例用应用侧 response processor handler 处理,但不是Zeek.Http内置 public API。
推荐把细节封装成后端级入口:
context.AddGtrApis(refitSettings);
context.AddErpApis(refitSettings);
推荐默认流程是“过期时间驱动懒刷新 + 401 forced refresh 兜底”:
如果后端有统一响应包裹,例如 GTR 的 { code, message, data } 或 ERP 的 { success, msg, result },解包应放在应用 API 模块自己的 handler / processor 中:
推荐规则:
- 登录和 refresh client 使用整体匿名 client,避免授权 provider 依赖登录 API 后形成 DI 或请求管线闭环。
- processor 对 401、403、5xx 这类 HTTP 非成功状态不要抢先吞掉,先让 retry / classifier 看见状态码。
- processor 只解释当前后端协议,不要把 GTR / ERP / 其它系统的 envelope 做成
Zeek.Http公共 API。 - UI 提示由 ViewModel 或 App service 统一处理;handler 只返回结果或抛出可读异常。
响应解包操作
解包不是“反序列化 DTO 前多写一行代码”这么简单。它要先判断 HTTP 状态,再判断业务 envelope,再决定把 data / result 交给 typed client 或 Refit。
推荐顺序:
- HTTP 非成功状态直接保留,例如
401交给 retry handler,403交给错误分类或业务提示。 - HTTP 成功后读取 JSON envelope。
- 判断业务成功码,例如 GTR
code == 100000,ERPsuccess == true。 - 业务失败时抛出后端自己的业务异常,异常里保留
code、message和必要 request 信息。 - 业务成功时只把
data/result的 JSON 交给后续 DTO 反序列化。
typed HttpClient 可以直接在 API client 内显式解包:
public async Task<OrderDto> GetOrderAsync(
long id,
CancellationToken cancellationToken = default)
{
using var response = await httpClient.GetAsync(
$"api/orders/{id}",
cancellationToken);
if (!response.IsSuccessStatusCode)
{
throw await GtrApiException.FromHttpAsync(response, cancellationToken);
}
return await GtrEnvelopeReader.ReadDataAsync(
response.Content,
GtrJsonContext.Default.OrderDto,
cancellationToken);
}
Refit 更适合把解包放在应用侧 DelegatingHandler 中,让 Refit interface 仍然返回业务 DTO:
public sealed class GtrEnvelopeHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
if (!response.IsSuccessStatusCode)
{
return response;
}
string json = await response.Content.ReadAsStringAsync(cancellationToken);
using JsonDocument document = JsonDocument.Parse(json);
JsonElement root = document.RootElement;
int code = root.GetProperty("code").GetInt32();
string? message = root.TryGetProperty("message", out JsonElement messageElement)
? messageElement.GetString()
: null;
if (code != 100000)
{
throw new GtrApiException(code, message ?? "服务端返回业务失败。");
}
string dataJson = root.TryGetProperty("data", out JsonElement dataElement)
? dataElement.GetRawText()
: "null";
response.Content.Dispose();
response.Content = new StringContent(
dataJson,
Encoding.UTF8,
"application/json");
return response;
}
}
上面代码是应用侧模式示例,不是 Zeek.Http 内置类型。生产代码通常还要补充:
- 使用 source-generated
JsonTypeInfo<T>读取 envelope 或 DTO,避免 NativeAOT 下依赖反射 JSON。 - 跳过文件下载、stream、图片、空响应、health check 等非 envelope 接口。
- 保留 response header、trace id、request uri、业务错误码,方便 UI 提示和日志排查。
- 不在 handler 里弹窗;让 ViewModel / App service 捕获
GtrApiException后统一提示。
完整的两种刷新方案、流程图、single-flight 伪代码、401 retry 伪代码、typed HttpClient 接线、response envelope 解包、request body 重放和生产检查清单见:
docs/topics/Zeek.Http 客户端授权与刷新指南.md
AOT JSON
Zeek.Http 推荐显式 JsonSerializerContext / JsonTypeInfo<T>:
[JsonSerializable(typeof(OrderDto))]
public partial class AppJsonContext : JsonSerializerContext;
var order = await response.Content.ReadZeekJsonAsync(
AppJsonContext.Default.OrderDto,
cancellationToken);
不要把反射 JSON fallback 当作 NativeAOT 主路径。
错误分类
通过 IZeekHttpErrorClassifier 把通用 HTTP 失败映射到 ZeekHttpErrorKind:
var error = errorClassifier.Classify(response);
if (error.Kind == ZeekHttpErrorKind.Unauthorized)
{
// 由业务或 UI 决定是否跳转登录。
}
Zeek.Http 只做通用分类,不解释业务错误码,不自动弹窗,不自动重新登录。
上传下载
IZeekHttpTransferService 提供基础 stream 传输:
var client = httpClientFactory.CreateClient(ZeekHttpOptions.DefaultEndpointName);
await using var destination = File.Create(path);
await transferService.DownloadAsync(
client,
"files/readme",
destination,
progress,
cancellationToken);
调用方仍拥有 stream 生命周期;模块不定义业务文件协议。
与 Refit 对接
业务项目推荐引用可选适配包:
<ItemGroup>
<PackageReference Include="Zeek.Http.Refit" />
</ItemGroup>
Zeek.Http.Refit 依赖 Refit;Zeek.Http 主包仍不依赖 Refit。
定义 Refit interface:
public interface IAuthApi
{
[Post("/auth/login")]
Task<LoginResponse> LoginAsync(
[Body] LoginRequest request,
CancellationToken cancellationToken = default);
}
public interface IOrderApi
{
[Get("/orders/{id}")]
Task<OrderDto> GetAsync(
int id,
CancellationToken cancellationToken = default);
}
业务模块依赖 Refit 适配模块并注册 Refit API:
public static IReadOnlyList<ModuleDependency> Dependencies =>
[ModuleDependency.Of<RefitModule>()];
var refitSettings = CreateRefitSettings();
context.AddZeekRefitAnonymousClient<IAuthApi>(refitSettings);
context.AddZeekRefitApi<BearerAuthorizationProvider>(refitSettings, api => api
.Add<IOrderApi>()
.Add<ICustomerApi>()
.Add<IProductApi>());
多系统时直接为每个 API client 绑定自己的 provider:
context.AddZeekRefitApi<IdentityAuthorizationProvider>("IdentityApi", refitSettings, api => api
.Add<IIdentityApi>());
context.AddZeekRefitApi<OrderAuthorizationProvider>("OrderApi", refitSettings, api => api
.Add<IOrderApi>()
.Add<ICustomerApi>());
context.AddZeekRefitApi<ReportAuthorizationProvider>("ReportApi", refitSettings, api => api
.Add<IReportApi>());
AddZeekRefitApi<TAuthorizationProvider>(...) 只是把多个 AddZeekRefitClient<TApi, TAuthorizationProvider>(...) 聚合到同一 endpoint 和同一 provider 下;每个 Refit interface 仍然显式注册,不扫描程序集。默认所有请求会尝试授权;受保护 client 中的少数匿名方法用 [AllowAnonymous] 标记,并映射到 ZeekHttpRequestOptions.AllowAnonymous。登录、刷新等整体匿名 API 优先使用 AddZeekRefitAnonymousClient<TApi>(...)。
Refit NativeAOT 场景应显式配置 source-generated serializer;Zeek.Http.Refit 只负责 Zeek handler 接线,不承诺 Refit upstream warning-clean:
private static RefitSettings CreateRefitSettings()
{
return new RefitSettings(
new SystemTextJsonContentSerializer(
new JsonSerializerOptions
{
TypeInfoResolver = AppJsonContext.Default,
}));
}
边界:
Zeek.Http.Refit只提供RefitModule和 Refit 注册捷径。Zeek.Http主包不依赖 Refit。- Refit attribute、接口代理、
ApiException和序列化行为仍由 Refit 自己承担。 - 不扫描程序集,不自动发现 API interface,不生成注册代码。
完整示例:
src/Zeek.Http.Refit/README.md
samples/Zeek.HttpRefitSample/
samples/Zeek.HttpRefitSample.Server/
docs/topics/Zeek.Http 使用指南.md
AOT / Trim
- AOT 支持等级:Required。
- Trim 策略:显式 endpoint、typed client、header provider、authorization provider 和
JsonTypeInfo<T>注册,不扫描程序集发现 API client。 - 反射策略:不把反射 JSON、动态代理或运行时 client 生成作为主路径。
Sample
../../samples/Zeek.HttpSample/../../samples/Zeek.HttpAuthSample/../../samples/Zeek.HttpRefitSample/../../samples/Zeek.HttpRefitAuthSample/
这些 sample 当前属于源码级落地模块验证路径,发布前可能仍通过本地 ProjectReference 消费源码项目。
完整文档
../../docs/modules/Zeek.Http.md../../docs/topics/Zeek.Http 使用指南.md../../docs/topics/Zeek.Http 客户端授权与刷新指南.md
当前限制
Zeek.Http尚未进入当前 BaGet 正式发布包清单。Zeek.Http不负责 token 保存、refresh、登录 UI、业务 envelope 平台或 Refit 主路径。
Showing the top 20 packages that depend on Zeek.Http.
| Packages | Downloads |
|---|---|
|
Zeek.Http.Refit
Refit integration helpers for Zeek.Http desktop applications.
|
19 |
|
Zeek.Http.Refit
Refit integration helpers for Zeek.Http desktop applications.
|
15 |
|
Zeek.Http.Refit
Refit integration helpers for Zeek.Http desktop applications.
|
12 |
|
Zeek.Http.Refit
Refit integration helpers for Zeek.Http desktop applications.
|
10 |
|
Zeek.Http.Refit
Refit integration helpers for Zeek.Http desktop applications.
|
5 |
|
Zeek.Http.Refit
Refit integration helpers for Zeek.Http desktop applications.
|
4 |
|
Zeek.Http.Refit
Refit integration helpers for Zeek.Http desktop applications.
|
3 |
|
Zeek.Http.Refit
Refit integration helpers for Zeek.Http desktop applications.
|
2 |
|
Zeek.Http.Refit
Refit integration helpers for Zeek.Http desktop applications.
|
1 |
.NET 10.0
- Zeek.Core (>= 10.0.25)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Http (>= 10.0.0)
- Microsoft.Extensions.Options (>= 10.0.0)
| Version | Downloads | Last updated |
|---|---|---|
| 10.0.42 | 4 | 07/29/2026 |
| 10.0.41 | 15 | 07/22/2026 |
| 10.0.40 | 10 | 07/22/2026 |
| 10.0.39 | 19 | 07/19/2026 |
| 10.0.38 | 12 | 07/17/2026 |
| 10.0.37 | 1 | 07/16/2026 |
| 10.0.36 | 5 | 06/29/2026 |
| 10.0.35 | 1 | 06/29/2026 |
| 10.0.31 | 5 | 06/27/2026 |
| 10.0.30 | 2 | 06/27/2026 |
| 10.0.29 | 2 | 06/26/2026 |
| 10.0.28 | 2 | 06/26/2026 |
| 10.0.27 | 2 | 06/26/2026 |
| 10.0.26 | 4 | 06/25/2026 |
| 10.0.25 | 3 | 06/24/2026 |