取消是控制流语义,不是失败。CancellationToken 必须正确传播,OperationCanceledException 绝不能被吞。
传播约定
接口定义
所有异步方法必须接受 CancellationToken 参数,默认值为 default:
public interface IPaymentProvider{ // ① ct = default 让消费者不传也能用,但实现内部必须传播 Task<PaymentResult<PaymentOrderResult>> CreateOrderAsync( PaymentOrderRequest request, CancellationToken ct = default);}实现内部传播
public async Task<PaymentResult<PaymentStatusResult>> QueryStatusAsync( string outTradeNo, CancellationToken ct = default){ // ① 方法入口校验取消(推荐先于参数校验) ct.ThrowIfCancellationRequested();
// ② 传递给下游 HttpClient 调用 var json = await _httpClient.PostAsync(MethodQuery, bizParams, ct);
// ③ 如果有多个 await,每个都要传 ct var response = await _httpClient.GetAsync(url, ct); var content = await response.Content.ReadAsStringAsync(ct); // ...}DI 注册时的传播
注册 HttpClient 时设置超时:
services.AddHttpClient(options.HttpClientName, client =>{ client.Timeout = options.Timeout; // ① 超时也会触发 CancellationToken}).AddRequestLogging(options.HttpClientName);OperationCanceledException 处理
正确做法(新代码)
public async Task<PaymentResult<T>> SomeOperationAsync(CancellationToken ct){ try { var result = await _httpClient.PostAsync(url, body, ct); return PaymentResult<T>.Success(result); } catch (OperationCanceledException) { // ① 取消不是失败,直接重抛让调用方知道 throw; } catch (PaymentException ex) { return PaymentResult<T>.Fail(ex.ErrorCode, ex.ProviderMessage); } catch (HttpRequestException ex) { return PaymentResult<T>.Fail("HTTP_ERROR", ex.Message); }}已知问题(旧代码)
部分 Sms 厂商实现(Aliyun/Huawei)的旧代码用兜底 catch (Exception) 会把取消异常吞成失败 Result:
// ❌ 错误做法——吞掉了取消异常catch (Exception ex){ return SMSResult.FromException(ex); // 取消异常被误标记为失败}新代码必须避免这种模式。
Rest 层的超时处理
RestClient 把超时映射为 RestException:
// RestClient.SendAsync 内部catch (OperationCanceledException ex) when (!ct.IsCancellationRequested){ // ① 如果不是消费者主动取消,说明是超时 throw new RestException( domain: "Rest", message: "请求超时", failureKind: IntegrationFailureKind.Timeout, isTransient: true, innerException: ex);}catch (OperationCanceledException){ // ② 消费者主动取消——正常重抛 throw;}RequestLogHandler 显式处理取消异常——记录耗时但不记为业务异常,直接重抛。