Polly 回退策略导致 "ObjectDisposedException: Cannot access a closed Stream" 第二次回退在 ASP.NET Core 3.1 上激活
Polly Fallback policy causes "ObjectDisposedException: Cannot access a closed Stream" the 2nd time fallback is activated on ASP.NET Core 3.1
我正在编写一个 REST API,它使用 Polly 弹性策略调用另一个 REST API。
我让回退策略第一次运行良好,但第二次触发时抛出错误:
System.IO.StreamHelpers.ValidateCopyToArgs(Stream source, Stream destination, int bufferSize)
System.IO.MemoryStream.CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
System.IO.Stream.CopyToAsync(Stream destination)
ProxyBase.HttpResponseMessageResult.ExecuteResultAsync(ActionContext context) in HttpResponseMessageResult.cs
+ await stream.CopyToAsync(context.HttpContext.Response.Body);
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|29_0<TFilter, TFilterAsync>(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext<TFilter, TFilterAsync>(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
HttpResponseMessageResult class 是一个自定义转换器,从客户端在回退处理程序中返回的 HttpResponseMessage 到控制器方法期望的 IActionResult。下面是抛出异常的方法代码:
context.HttpContext.Response.StatusCode = (int)_responseMessage.StatusCode;
using (var stream = await _responseMessage.Content.ReadAsStreamAsync())
{
await stream.CopyToAsync(context.HttpContext.Response.Body);
await context.HttpContext.Response.Body.FlushAsync();
}
这是我的政策:
var fallbackPolicy = Policy<HttpResponseMessage>.Handle<Exception>()
.OrResult(r => r.StatusCode == System.Net.HttpStatusCode.RequestTimeout)
.FallbackAsync(
GetFailoverResponse(fallbackUrl),
(ct,cx) => {
HttpClient client = new HttpClient();
return client.GetAsync(fallbackUrl + ct.Result.RequestMessage.RequestUri.AbsolutePath);
});
clientBuilder.AddPolicyHandler(fallbackPolicy);
我第二次检查返回的 HttpResponseMessage 是新鲜的 - 它是独一无二的,我可以看到当我添加一个随机参数时 queryString 是不同的。
我还尝试用回退调用直接替换实际失败的调用(即让 Polly 脱离等式)- 这样它工作正常,所以 Polly 出于某种原因用关闭的流传递 HttpResponseMessage。
问题似乎与(滥用)使用 FallbackAsync 方法的第二个参数有关:它意味着 运行 在 return 回退结果之前 运行 但不是为了生成结果本身,这就是我在上面代码的损坏版本中所做的。
该方法有各种重载,但它们主要支持两个参数并且在确切的数据类型上有所不同:fallbackResult 和 onFallbackAsync。在损坏的示例中,我 return 将静态 fallbackResult 作为备份,即使我的 onFallbackAsync 委托 returns 任务。我的(错误的)理解是,这将是将由策略 return 编辑的 HTTP 响应。显然,在引擎盖下它实际上是 return 静态响应,它在第二次使用时已经读取了它的流。
解决方案是使用第一个参数的另一个重载,它不会 return 静态 HttpResponseMessage 但会将 HTTP 请求上下文传递给可以执行回退 HTTP 请求的 lambda,如下所示:
var fallbackPolicy = Policy<HttpResponseMessage>.Handle<Exception>()
.OrResult(r => r.StatusCode == System.Net.HttpStatusCode.RequestTimeout)
.FallbackAsync(
(res, cx, ct) => {
HttpClient client = new HttpClient();
return client.GetAsync(fallbackUrl + res.Result.RequestMessage.RequestUri.AbsolutePath);
},
(ct, cx) =>
{
return Task.CompletedTask;
});
我正在编写一个 REST API,它使用 Polly 弹性策略调用另一个 REST API。 我让回退策略第一次运行良好,但第二次触发时抛出错误:
System.IO.StreamHelpers.ValidateCopyToArgs(Stream source, Stream destination, int bufferSize)
System.IO.MemoryStream.CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
System.IO.Stream.CopyToAsync(Stream destination)
ProxyBase.HttpResponseMessageResult.ExecuteResultAsync(ActionContext context) in HttpResponseMessageResult.cs
+ await stream.CopyToAsync(context.HttpContext.Response.Body);
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|29_0<TFilter, TFilterAsync>(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext<TFilter, TFilterAsync>(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
HttpResponseMessageResult class 是一个自定义转换器,从客户端在回退处理程序中返回的 HttpResponseMessage 到控制器方法期望的 IActionResult。下面是抛出异常的方法代码:
context.HttpContext.Response.StatusCode = (int)_responseMessage.StatusCode;
using (var stream = await _responseMessage.Content.ReadAsStreamAsync())
{
await stream.CopyToAsync(context.HttpContext.Response.Body);
await context.HttpContext.Response.Body.FlushAsync();
}
这是我的政策:
var fallbackPolicy = Policy<HttpResponseMessage>.Handle<Exception>()
.OrResult(r => r.StatusCode == System.Net.HttpStatusCode.RequestTimeout)
.FallbackAsync(
GetFailoverResponse(fallbackUrl),
(ct,cx) => {
HttpClient client = new HttpClient();
return client.GetAsync(fallbackUrl + ct.Result.RequestMessage.RequestUri.AbsolutePath);
});
clientBuilder.AddPolicyHandler(fallbackPolicy);
我第二次检查返回的 HttpResponseMessage 是新鲜的 - 它是独一无二的,我可以看到当我添加一个随机参数时 queryString 是不同的。
我还尝试用回退调用直接替换实际失败的调用(即让 Polly 脱离等式)- 这样它工作正常,所以 Polly 出于某种原因用关闭的流传递 HttpResponseMessage。
问题似乎与(滥用)使用 FallbackAsync 方法的第二个参数有关:它意味着 运行 在 return 回退结果之前 运行 但不是为了生成结果本身,这就是我在上面代码的损坏版本中所做的。
该方法有各种重载,但它们主要支持两个参数并且在确切的数据类型上有所不同:fallbackResult 和 onFallbackAsync。在损坏的示例中,我 return 将静态 fallbackResult 作为备份,即使我的 onFallbackAsync 委托 returns 任务。我的(错误的)理解是,这将是将由策略 return 编辑的 HTTP 响应。显然,在引擎盖下它实际上是 return 静态响应,它在第二次使用时已经读取了它的流。
解决方案是使用第一个参数的另一个重载,它不会 return 静态 HttpResponseMessage 但会将 HTTP 请求上下文传递给可以执行回退 HTTP 请求的 lambda,如下所示:
var fallbackPolicy = Policy<HttpResponseMessage>.Handle<Exception>()
.OrResult(r => r.StatusCode == System.Net.HttpStatusCode.RequestTimeout)
.FallbackAsync(
(res, cx, ct) => {
HttpClient client = new HttpClient();
return client.GetAsync(fallbackUrl + res.Result.RequestMessage.RequestUri.AbsolutePath);
},
(ct, cx) =>
{
return Task.CompletedTask;
});