Skip to content

错误处理指南 ​

本文说明 Mud.Feishu SDK 的错误通道设计,重点覆盖文件下载类接口(Task<byte[]?>)这一 与其余接口语义不同的特例。

1. 统一响应模型(绝大多数接口) ​

除文件下载外,所有接口返回 FeishuApiResult<T>?(或 FeishuApiListResult<T>? / FeishuApiPageListResult<T>? / FeishuNullDataApiResult?):

csharp
var result = await _messageApi.SendMessageAsync(request, "open_id");
if (result is { Code: 0 })
{
    // 成功
}
else
{
    // 业务错误:result.Code / result.Msg
}

注意:HTTP 200 且 Code != 0 是飞书最常见的业务错误形态,此时不会抛异常, 必须显式检查 Code。

2. 传输层错误 ​

服务端返回非 2xx 状态码时,HTTP 执行器统一抛出 Mud.HttpUtils.ApiException:

属性说明
StatusCodeHTTP 状态码(401 / 403 / 404 / 429 / 5xx 等)
Content响应体内容(已按 MaxExceptionContentLength 截断;敏感内容经 IExceptionRedactor 擦除)
RequestUri请求地址(已脱敏)
RequestContent捕获的请求体(仅在启用 CaptureRequestContent 时填充)
csharp
try
{
    var result = await _userApi.GetUserAsync("ou_xxx");
}
catch (ApiException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
{
    // 触发飞书限流:按 Retry-After 退避重试
}

401 由 SDK 自动恢复:应用上下文中的 TokenRecoveryEnhancedClient 会刷新令牌并重试一次, 业务代码通常无需处理(仅在自行调用认证接口获取令牌的场景下需要留意)。

3. 文件下载类接口(Task<byte[]?>) ​

以下 9 个方法直接返回二进制内容,不经过 FeishuApiResult<T>,因此其错误语义需要单独说明:

模块方法
IFeishuV1Message_TenantGetMessageFile、DownFileAsync、DownImageAsync
IFeishuV1DriveFilesDownloadFileAsync、DownloadExportFileAsync
IFeishuV1DriveMediaDownloadFileAsync
IFeishuV1BoardDownloadWhiteboardImageAsync
IFeishuV1VideoConferencingExportsDownloadExportAsync
IFeishuV1AttendanceUserSettings_TenantDownloadFileAsync
IFeishuV1HelpDeskTicket_TenantGetTicketImageAsync

行为契约(已由 Tests/Mud.Feishu.Tests/Http/DownloadErrorSemanticsTests.cs 锁定)

  1. 服务端返回 非 2xx → 抛 ApiException(Content 为错误响应体)。 调用方无需区分「失败」与「成功但内容为空」,前者是异常。
  2. 服务端返回 2xx → 原样返回响应体字节(可空签名,实际不会为 null;空响应体对应空数组)。
  3. 残余风险:HTTP 200 + JSON 错误体。飞书部分业务错误以 HTTP 200 配合 {"code":99991672,"msg":"..."} 返回,此时本方法会把错误 JSON 的字节当作文件内容返回。 执行器不做内容嗅探(无法在不破坏二进制语义的前提下区分),因此需要调用方自检:
csharp
var bytes = await _driveFilesApi.DownloadFileAsync(fileToken);
if (bytes is null || bytes.Length == 0)
{
    throw new InvalidOperationException("下载内容为空");
}

// 低价自检:错误响应体总是 JSON,且以 '{' 开头
if (bytes.Length > 1 && bytes[0] == (byte)'{')
{
    var errorJson = System.Text.Encoding.UTF8.GetString(bytes);
    throw new InvalidOperationException($"下载失败,服务端返回错误体:{errorJson}");
}

await File.WriteAllBytesAsync(localPath, bytes);

若需要更严格的判定,可改用 IFeishuV1DriveFiles 等接口上形如 Task<FeishuApiResult<...>?> 的元数据接口先校验文件是否存在 / 是否有权限, 再下载内容。

4. 异常类型速查 ​

异常触发场景
ApiException非 2xx 状态码;JSON/XML 反序列化失败
ApiRequestException传输层失败(DNS / TLS / 连接被拒),或成功响应体超出 MaxSuccessResponseBytes
TaskCanceledException请求超时(HttpClient.Timeout 或弹性策略超时)或调用方取消
InvalidOperationException配置缺失(如使用 [Body(EnableEncrypt=true)] 但未注册 IEncryptionProvider)

5. 相关配置 ​

配置项作用
FeishuAppConfig.TimeoutSeconds命名客户端超时(秒),支持配置热更新
FeishuAppConfig.HttpRetry.MaxAttempts / HttpRetry.DelayMs弹性重试(指数退避)
FeishuAppConfig.CircuitBreaker.*熔断策略
EnhancedHttpClientOptions.MaxSuccessResponseBytes成功响应体上限(防 OOM,0 = 不限制)
EnhancedHttpClientOptions.MaxExceptionContentLength异常内容截断长度
EnhancedHttpClientOptions.CaptureRequestContent是否捕获请求体用于诊断(含敏感数据,慎用)

6. 事件处理投递语义(at-least-once) ​

WebSocket / Webhook 事件链路不追求严格一次,契约为 at-least-once + 尽力幂等。 业务失败或服务端超时后,飞书可能重发;处理器必须幂等,或以业务唯一键兜底 (IdempotentFeishuEventHandler 默认业务键为 "{HandlerType}:{EventId}",禁止返回裸 EventId)。

去重所有权分层 ​

层级负责组件标识失败时
传输层WS BinaryMessageProcessor + MessageSequenceValidator + SeqID 去重SeqID统一回滚窗口/SeqID 标记,ACK 500 触发重发
事件层FeishuEventMessageHandler / FeishuWebhookServiceEventId回滚 processing 态;Mark 失败不回滚(WHF-07)

两通道终态对照 ​

终态WebSocketWebhook
业务成功Mark completed → ACK 200Mark completed → 200
业务失败回滚 + ACK 500(重发)回滚 + 失败存储 + 500(或按配置返回)
Mark 失败保留 processing,ACK 200(WHF-07)保留 processing,按成功返回
被拦截回滚去重 + 重抛 → ACK 500(retry-until-accept)不进去重 + (false,"Event intercepted") → 500
空 EventId默认拒绝(RejectEmptyEventIds=true)默认 400(RejectEmptyIdentifiers=true)
未注册事件默认回退默认处理器;IgnoreUnknownEventTypes=true 时忽略默认忽略(IgnoreUnknownEventTypes=true)
去重体系致命故障(WS 无对应 WHF-02 中间件转换)FeishuDeduplicationFatalException → 不回滚/不写失败存储 → 503
外部取消 / 超时OCE 传播(ACK 500/停机)超时就地收尾;外部取消回滚后 rethrow

后置拦截器 AfterHandleAsync(exception):业务成功为 null; 拦截/取消等特殊终态可能收到 EventHandlingOutcomeException(OutcomeKind 为 intercepted/canceled/dedup_fatal)。

失败重试与扇出 ​

  • 启用 Retry.EnableRetry 时,业务失败会写入 IFailedEventStore(ADR-2);FailedEventInfo 可含 SerializedHeader/StoreKey。
  • 多处理器扇出任一失败将整体回滚去重并依赖重发,已成功的处理器会重复执行(见 IFeishuEventHandlerFactory.HandleEventParallelAsync XML)。