Skip to content
Bitzsoft.Integrationsbitzsoft.integrations

Concept

错误处理

域异常体系、Result 与 Exception 两种范式、IntegrationException 基类与 FailureKind 枚举、OperationCanceledException 处理与配置校验 fail-fast。

Last updated

错误处理是连接器库设计中最需要明确边界的领域。第三方 API 失败是常态而非异常,如何把厂商各异的错误结构传递给消费者、何时用异常何时用返回值、取消操作如何处理——本节定义全库统一的错误处理约定。

两种错误处理范式

全库存在两种对外暴露失败的范式,由 ADR-0004 确立推荐方向。

范式机制适用代表域
Result(推荐)返回 Result<T>,含 IsSuccess + ErrorCode + ErrorMessage新域Payment、OutboundCall
Exception(遗留)直接抛 DomainException,消费者 try/catch历史遗留TeamWork
Exception 范式(遗留)

Provider 直接抛

消费者 try/catch

Result 范式(推荐)

Provider 内部抛异常

方法边界 catch

转 Result.Fail 返回

为什么 Result 优于 Exception

第三方 API 调用失败不是”异常情况”而是”常见分支”。用 Result<T> 返回结构化的错误码和消息,比裸异常更易被消费者程序化处理——映射到 HTTP 响应、记日志、判断是否重试。内部仍用异常做控制流,但在 Provider 方法边界统一捕获并转换。

域异常体系

没有统一异常根

全库没有单一的全局异常基类供消费者 catch。每个域定义自己的公开异常类型,保证域内自治。

异常类基类
PaymentPaymentExceptionIntegrationException
TeamWorkTeamWorkExceptionIntegrationException
OutboundCallOutboundCallExceptionIntegrationException
FileStorageFileStorageExceptionIntegrationException
ExpressExpressExceptionIntegrationException
Finance.InvoiceInvoiceExceptionIntegrationException

域异常的结构

每个域异常封装供应商原始错误码和消息,继承 IntegrationException 的结构化字段:

public class PaymentException : IntegrationException
{
public string ProviderName => Provider ?? string.Empty; // ① 供应商名称
public new string? ErrorCode => base.ErrorCode; // ② 厂商错误码
public new string? ProviderMessage => base.ProviderMessage; // ③ 厂商原始消息
public PaymentException(string providerName, string? errorCode, string? providerMessage)
: base(
domain: "Payment",
message: $"[{providerName}] {errorCode}: {providerMessage}",
provider: providerName,
errorCode: errorCode,
providerMessage: providerMessage)
{ }
}

IntegrationException 基类

IntegrationException 是 Core 包提供的可选统一基类,为宿主提供跨域的结构化错误信息。

核心字段

public class IntegrationException : Exception
{
public string Domain { get; } // ① 域名(Payment、TeamWork...)
public string? Provider { get; } // ② 供应商标识
public string? ErrorCode { get; } // ③ 错误码
public string? ProviderMessage { get; } // ④ 厂商原始消息
public IntegrationFailureKind FailureKind { get; } // ⑤ 稳定失败分类
public int? HttpStatusCode { get; } // ⑥ HTTP 状态码(可用时)
public string? RequestId { get; } // ⑦ 追踪 ID
public bool IsTransient { get; } // ⑧ 是否可重试
public TimeSpan? RetryAfter { get; } // ⑨ 建议重试等待
}

IntegrationFailureKind 枚举

FailureKind 是 13 个值的稳定分类枚举,供宿主统一映射状态码、告警和重试策略,无需解析各厂商的字符串错误码:

含义典型场景
Unknown无法分类未预期错误
Configuration配置缺失或无效密钥为空、URL 格式错
Authentication认证失败Token 过期、密钥错
Authorization权限不足无操作权限
Validation参数校验失败金额非法、格式错
NotFound资源不存在订单号不存在
Conflict状态冲突重复提交
RateLimited限流触发 QPS 上限
Network网络层失败DNS、TLS、连接超时
Timeout调用超时请求超时
Provider厂商业务/服务端错误厂商返回业务错误
Unsupported能力不支持当前 Provider 不支持该场景
Cancelled调用被取消CancellationToken 触发

IsTransient 与 RetryAfter

IsTransient 标识失败是否可在满足幂等性约束后重试。RetryAfter 是供应商建议的最短重试等待(如限流时返回的 Retry-After 头)。消费者据此实现重试策略:

try
{
await provider.DoSomethingAsync(ct);
}
catch (IntegrationException ex) when (ex.IsTransient)
{
// ① 瞬时错误——可重试
var delay = ex.RetryAfter ?? TimeSpan.FromSeconds(Math.Pow(2, attempt));
await Task.Delay(delay, ct);
// 重试...
}
catch (IntegrationException ex)
{
// ② 永久错误——不重试,告警
logger.LogError("永久失败: {Code}", ex.ErrorCode);
}

Result 范式(推荐)

Payment 域的标准模式

Payment 域是 Result 范式的标准范例。PaymentResult<T> 提供 IsSuccess / Data / ErrorCode / ErrorMessageSuccess() / Fail() 工厂方法:

public async Task<PaymentResult<PaymentStatusResult>> QueryStatusAsync(
string outTradeNo, CancellationToken ct)
{
try
{
var json = await _httpClient.GetAsync($"{Path}/{outTradeNo}", ct);
// ① 业务码非成功 → 直接返回 Fail
if (GetStr(json, "code") != "success")
return PaymentResult<PaymentStatusResult>.Fail("QUERY_FAILED", GetStr(json, "msg"));
return PaymentResult<PaymentStatusResult>.Success(result);
}
catch (PaymentException ex)
{
// ② 领域异常 → 提取 ErrorCode 和 ProviderMessage
return PaymentResult<PaymentStatusResult>.Fail(ex.ErrorCode ?? "QUERY_FAILED", ex.ProviderMessage ?? ex.Message);
}
catch (HttpRequestException ex)
{
// ③ 网络异常 → 统一错误码
return PaymentResult<PaymentStatusResult>.Fail("HTTP_ERROR", ex.Message);
}
}

catch 顺序约定

Provider 方法的 catch 块按从具体到一般的顺序排列:

领域业务错误不匹配网络错误不匹配

try: 调用第三方 API

catch DomainException

Result.Fail(ErrorCode, ProviderMessage)

catch HttpRequestException

Result.Fail(HTTP_ERROR, ...)

(OperationCanceledException 透传)

  1. catch (DomainException):捕获 HttpClient 封装层抛出的领域异常,提取结构化错误码。
  2. catch (HttpRequestException):捕获网络层异常,统一映射为 HTTP_ERROR
  3. OperationCanceledException 不在此处捕获——见下文。

Exception 范式(遗留)

TeamWork 域直接抛 TeamWorkException,消费者必须 try/catch。这是历史遗留范式,ADR-0004 明确不改造。

// TeamWork 域——Provider 直接抛异常
public async Task<...> CreateApprovalAsync(...)
{
var json = await _httpClient.PostAsync(...);
if (GetStr(json, "errcode") != "0")
throw new TeamWorkException("dingtalk", GetStr(json, "errcode"), GetStr(json, "errmsg"));
// ...
}
// 消费者——必须 try/catch
try
{
await teamWork.CreateApprovalAsync(...);
}
catch (TeamWorkException ex)
{
// 处理失败
}

OperationCanceledException 处理

核心规则:永不吞掉

OperationCanceledException(含 TaskCanceledException)表示调用被消费者主动取消,是控制流语义,不是失败。新代码必须让它透传,绝不吞掉。

// ✓ 正确——用异常过滤器排除取消异常
try
{
await provider.CallVendorAsync(ct);
}
catch (Exception ex) when (ex is not OperationCanceledException) // ① 排除取消异常
{
// 只处理真正的失败
return Result.Fail(...);
}
// 取消异常自然透传给消费者
// ✗ 禁止——吞掉取消异常会破坏取消语义
try
{
await provider.CallVendorAsync(ct);
}
catch (Exception ex) // ① 这会捕获 OperationCanceledException,破坏取消
{
return Result.Fail("CANCELLED", ex.Message); // ✗ 错误!
}

Twilio 的正确示例

OutboundCall.Twilio 用异常过滤器正确处理取消:

catch (Exception ex) when (ex is not OperationCanceledException)
{
// ① 只处理非取消异常,取消自然向上传播
throw new OutboundCallException(..., innerException: ex);
}

配置校验:fail-fast

配置校验是 Result 范式的唯一例外——配置错误属于编程错误,必须 fail-fast 抛异常,不返回 Result。

编程错误预期分支

配置错误

直接抛异常
ValidateOptionsResult.Fail

启动期 / 首次解析时崩溃

API 调用失败

返回 Result.Fail

消费者处理

为什么配置校验抛异常

配置错误(密钥为空、URL 格式错)不是运行时可恢复的失败,而是部署错误。fail-fast 能在应用启动时就暴露问题,避免带着错误配置进入生产后逐次调用失败。详见 Options 配置与校验

// 配置校验——抛异常(通过 IValidateOptions.Fail)
internal sealed class DingTalkOptionsValidator : IValidateOptions<DingTalkOptions>
{
public ValidateOptionsResult Validate(string? name, DingTalkOptions options)
{
if (string.IsNullOrWhiteSpace(options.AppKey))
return ValidateOptionsResult.Fail("钉钉 AppKey 不能为空"); // ① 启动期抛异常
// ...
}
}

错误处理决策树

配置错误调用被取消厂商业务错误网络错误Result(新域)Exception(遗留)

Provider 方法

错误类型?

抛异常 fail-fast
IValidateOptions.Fail

透传 OperationCanceledException
绝不吞

HttpClient 层抛 DomainException

HttpClient 层抛 HttpRequestException

方法边界 catch

域范式?

转 Result.Fail(ErrorCode, Message)

透传 DomainException

相关约定

100%

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