Skip to content

Commit 0cbdf12

Browse files
authored
Ignore response transform OCEs that occur during error handling (dotnet#2452)
1 parent 7d471c8 commit 0cbdf12

File tree

5 files changed

+104
-7
lines changed

5 files changed

+104
-7
lines changed

src/ReverseProxy/Forwarder/HttpForwarder.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Collections.Generic;
66
using System.Diagnostics;
77
using System.IO;
8-
using System.Linq;
98
using System.Net;
109
using System.Net.Http;
1110
using System.Threading;
@@ -15,7 +14,6 @@
1514
#if NET8_0_OR_GREATER
1615
using Microsoft.AspNetCore.Http.Timeouts;
1716
#endif
18-
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
1917
using Microsoft.AspNetCore.WebUtilities;
2018
using Microsoft.Extensions.Logging;
2119
using Microsoft.Net.Http.Headers;
@@ -643,7 +641,16 @@ private async ValueTask<ForwarderError> HandleRequestFailureAsync(HttpContext co
643641
{
644642
var error = HandleRequestBodyFailure(context, requestBodyCopyResult, requestBodyException!, requestException,
645643
timedOut: requestCancellationSource.IsCancellationRequested);
646-
await transformer.TransformResponseAsync(context, proxyResponse: null, requestCancellationSource.Token);
644+
645+
try
646+
{
647+
await transformer.TransformResponseAsync(context, proxyResponse: null, requestCancellationSource.Token);
648+
}
649+
catch (OperationCanceledException)
650+
{
651+
// We're about to report a more specific error, so ignore OCEs that occur here.
652+
}
653+
647654
return error;
648655
}
649656
}
@@ -669,7 +676,16 @@ async ValueTask<ForwarderError> ReportErrorAsync(ForwarderError error, int statu
669676
await requestContent.ConsumptionTask;
670677
}
671678

672-
await transformer.TransformResponseAsync(context, null, requestCancellationSource.Token);
679+
try
680+
{
681+
await transformer.TransformResponseAsync(context, proxyResponse: null, requestCancellationSource.Token);
682+
}
683+
catch (OperationCanceledException)
684+
{
685+
// We may have manually cancelled the request CTS as part of error handling.
686+
// We're about to report a more specific error, so ignore OCEs that occur here.
687+
}
688+
673689
return error;
674690
}
675691
}

src/ReverseProxy/SessionAffinity/BaseEncryptedSessionAffinityPolicy.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,19 @@ protected BaseEncryptedSessionAffinityPolicy(IDataProtectionProvider dataProtect
2626

2727
public abstract string Name { get; }
2828

29-
public virtual void AffinitizeResponse(HttpContext context, ClusterState cluster, SessionAffinityConfig config, DestinationState destination)
29+
public void AffinitizeResponse(HttpContext context, ClusterState cluster, SessionAffinityConfig config, DestinationState destination)
3030
{
3131
if (!config.Enabled.GetValueOrDefault())
3232
{
3333
throw new InvalidOperationException($"Session affinity is disabled for cluster.");
3434
}
3535

36+
if (context.RequestAborted.IsCancellationRequested)
37+
{
38+
// Avoid wasting time if the client is already gone.
39+
return;
40+
}
41+
3642
// Affinity key is set on the response only if it's a new affinity.
3743
if (!context.Items.ContainsKey(AffinityKeyId))
3844
{

src/ReverseProxy/SessionAffinity/BaseHashCookieSessionAffinityPolicy.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ public void AffinitizeResponse(HttpContext context, ClusterState cluster, Sessio
3131
throw new InvalidOperationException("Session affinity is disabled for cluster.");
3232
}
3333

34+
if (context.RequestAborted.IsCancellationRequested)
35+
{
36+
// Avoid wasting time if the client is already gone.
37+
return;
38+
}
39+
3440
// Affinity key is set on the response only if it's a new affinity.
3541
if (!context.Items.ContainsKey(AffinityKeyId))
3642
{

src/ReverseProxy/SessionAffinity/ISessionAffinityPolicy.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,6 @@ ValueTask<AffinityResult> FindAffinitizedDestinationsAsync(HttpContext context,
6565
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
6666
ValueTask AffinitizeResponseAsync(HttpContext context, ClusterState cluster, SessionAffinityConfig config, DestinationState destination, CancellationToken cancellationToken)
6767
{
68-
cancellationToken.ThrowIfCancellationRequested();
69-
7068
AffinitizeResponse(context, cluster, config, destination);
7169
return default;
7270
}

test/ReverseProxy.Tests/Forwarder/HttpForwarderTests.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2696,6 +2696,77 @@ public async Task RequestFailure_ResponseTransformsAreCalled(bool failureInReque
26962696
events.AssertContainProxyStages(failureInRequestTransform ? Array.Empty<ForwarderStage>() : new [] { ForwarderStage.SendAsyncStart });
26972697
}
26982698

2699+
[Theory]
2700+
[InlineData(true)]
2701+
[InlineData(false)]
2702+
public async Task RequestFailure_CancellationExceptionInResponseTransformIsIgnored(bool throwOce)
2703+
{
2704+
var events = TestEventListener.Collect();
2705+
2706+
var httpContext = new DefaultHttpContext();
2707+
httpContext.Request.Method = "GET";
2708+
httpContext.Request.Host = new HostString("example.com:3456");
2709+
2710+
var destinationPrefix = "https://localhost:123/";
2711+
var sut = CreateProxy();
2712+
var client = MockHttpHandler.CreateClient(
2713+
(HttpRequestMessage request, CancellationToken cancellationToken) =>
2714+
{
2715+
throw new Exception();
2716+
});
2717+
2718+
var responseTransformWithNullResponseCalled = false;
2719+
2720+
var transformer = new DelegateHttpTransforms
2721+
{
2722+
OnResponse = (context, response) =>
2723+
{
2724+
if (response is null)
2725+
{
2726+
responseTransformWithNullResponseCalled = true;
2727+
2728+
throw throwOce
2729+
? new OperationCanceledException("Foo")
2730+
: new InvalidOperationException("Bar");
2731+
}
2732+
2733+
return new ValueTask<bool>(true);
2734+
}
2735+
};
2736+
2737+
var proxyError = ForwarderError.None;
2738+
Exception exceptionThrownBySendAsync = null;
2739+
2740+
try
2741+
{
2742+
proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, transformer);
2743+
}
2744+
catch (Exception ex)
2745+
{
2746+
exceptionThrownBySendAsync = ex;
2747+
}
2748+
2749+
Assert.True(responseTransformWithNullResponseCalled);
2750+
2751+
if (throwOce)
2752+
{
2753+
Assert.Null(exceptionThrownBySendAsync);
2754+
Assert.Equal(ForwarderError.Request, proxyError);
2755+
}
2756+
else
2757+
{
2758+
Assert.NotNull(exceptionThrownBySendAsync);
2759+
}
2760+
2761+
Assert.Equal(StatusCodes.Status502BadGateway, httpContext.Response.StatusCode);
2762+
var errorFeature = httpContext.Features.Get<IForwarderErrorFeature>();
2763+
Assert.Equal(ForwarderError.Request, errorFeature.Error);
2764+
Assert.IsType<Exception>(errorFeature.Exception);
2765+
2766+
AssertProxyStartFailedStop(events, destinationPrefix, httpContext.Response.StatusCode, errorFeature.Error);
2767+
events.AssertContainProxyStages([ForwarderStage.SendAsyncStart]);
2768+
}
2769+
26992770
public enum CancellationScenario
27002771
{
27012772
RequestAborted,

0 commit comments

Comments
 (0)