使用Refit实现性能日志时的远程API调用异常排查
核心现象
在TimedOperationAsync方法中调用Refit客户端的LogAsync,虽然返回状态码200,但远程服务器未收到日志;单独调用LogAsync则正常工作。
可能的原因及解决方法
1. Refit接口的[Body]属性未指定Raw序列化方式
Refit默认会将[Body]参数序列化为JSON,但你的参数是StringContent(已经是序列化后的JSON内容),此时Refit会错误地将StringContent对象本身序列化为JSON,而非发送其内部的日志内容。服务器收到的是StringContent的对象描述,而非实际日志,因此不会写入日志,但可能返回200(若服务器不校验请求体格式)。
解决方法:修改Refit接口,指定BodySerializationMethod.Raw:
public interface MyRefitClient { [Post("/api/log")] Task<IApiResponse> LogAsync([Body(BodySerializationMethod.Raw)] StringContent request, CancellationToken cancellation); }
2. StringContent的流位置问题
StringContent内部的内存流在创建后,Position可能处于流的末尾,导致Refit读取不到内容。虽然这种情况较少见,但可以通过重置流位置解决。
解决方法:创建StringContent后重置流位置:
var json = JsonSerializer.Serialize(request); var content = new StringContent(json, Encoding.UTF8, "application/json"); // 重置流位置到起始处 await content.ReadAsStreamAsync().ContinueWith(s => s.Result.Position = 0); var logResponse = await _myRefitClient.LogAsync(content, cts);
3. 调用方代码的笔误(示例代码可见)
在Foo.DoSomethingAsync的示例代码中,你错误地调用了_myRefitClient.TimedOperationAsync,但MyRefitClient接口并没有这个方法。实际运行中如果是这个错误,代码应该无法编译,推测是你编写示例时的笔误,但如果实际代码存在类似错误,会导致调用逻辑异常。
解决方法:确保调用的是_benchmarkingService.TimedOperationAsync:
public async Task DoSomethingAsync() { var result = await _benchmarkingService.TimedOperationAsync(async () => { return await DoOtherThingAsync(); }, "Doing something", new Dictionary<string, string>(), CancellationToken.None); }
4. CancellationToken提前取消
虽然你传入的是CancellationToken.None,但如果在TimedOperationAsync执行过程中,外部的CancellationToken被取消(比如实际场景中并非传入None),可能导致请求被静默终止,但Refit仍返回200(取决于服务器处理逻辑)。
解决方法:检查CancellationToken的生命周期,确保在LogAsync调用时未被取消;可以在调用前添加校验:
cts.ThrowIfCancellationRequested(); var logResponse = await _myRefitClient.LogAsync(content, cts);
验证建议
- 启用Refit的日志功能,查看实际发送的请求体内容,确认是否为预期的日志JSON。
- 使用抓包工具(如Fiddler/Charles)对比
TimedOperationAsync中调用和单独调用的请求体差异,快速定位问题。
内容的提问来源于stack exchange,提问作者envyM6

