Skip to content

Commit 01715d1

Browse files
authored
Forwarder blame improvements (#2167)
1 parent 07955da commit 01715d1

File tree

6 files changed

+161
-132
lines changed

6 files changed

+161
-132
lines changed

src/ReverseProxy/Forwarder/HttpForwarder.cs

Lines changed: 40 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ public async ValueTask<ForwarderError> SendAsync(
416416

417417
// :: Step 2: Setup copy of request body (background) Client --► Proxy --► Destination
418418
// Note that we must do this before step (3) because step (3) may also add headers to the HttpContent that we set up here.
419-
var requestContent = SetupRequestBodyCopy(context.Request, isStreamingRequest, activityToken);
419+
var requestContent = SetupRequestBodyCopy(context, isStreamingRequest, activityToken);
420420
destinationRequest.Content = requestContent;
421421

422422
// :: Step 3: Copy request headers Client --► Proxy --► Destination
@@ -496,12 +496,13 @@ private void FixupUpgradeRequestHeaders(HttpContext context, HttpRequestMessage
496496
// else not an upgrade, or H2->H2, no changes needed
497497
}
498498

499-
private StreamCopyHttpContent? SetupRequestBodyCopy(HttpRequest request, bool isStreamingRequest, ActivityCancellationTokenSource activityToken)
499+
private StreamCopyHttpContent? SetupRequestBodyCopy(HttpContext context, bool isStreamingRequest, ActivityCancellationTokenSource activityToken)
500500
{
501501
// If we generate an HttpContent without a Content-Length then for HTTP/1.1 HttpClient will add a Transfer-Encoding: chunked header
502502
// even if it's a GET request. Some servers reject requests containing a Transfer-Encoding header if they're not expecting a body.
503503
// Try to be as specific as possible about the client's intent to send a body. The one thing we don't want to do is to start
504504
// reading the body early because that has side-effects like 100-continue.
505+
var request = context.Request;
505506
var hasBody = true;
506507
var contentLength = request.Headers.ContentLength;
507508
var method = request.Method;
@@ -512,8 +513,9 @@ private void FixupUpgradeRequestHeaders(HttpContext context, HttpRequestMessage
512513
// 5.0 servers provide a definitive answer for us.
513514
hasBody = canHaveBodyFeature.CanHaveBody;
514515

515-
// TODO: Kestrel bug, this shouldn't be true for ExtendedConnect.
516-
#if NET7_0_OR_GREATER
516+
#if NET7_0
517+
// TODO: Kestrel 7.0 bug only, hasBody shouldn't be true for ExtendedConnect.
518+
// https://github.com/dotnet/aspnetcore/issues/46002 Fixed in 8.0
517519
var connectFeature = request.HttpContext.Features.Get<IHttpExtendedConnectFeature>();
518520
if (connectFeature?.IsExtendedConnect == true)
519521
{
@@ -560,31 +562,13 @@ private void FixupUpgradeRequestHeaders(HttpContext context, HttpRequestMessage
560562

561563
if (hasBody)
562564
{
563-
if (isStreamingRequest)
564-
{
565-
DisableMinRequestBodyDataRateAndMaxRequestBodySize(request.HttpContext);
566-
}
567-
568-
// Note on `autoFlushHttpClientOutgoingStream: isStreamingRequest`:
569-
// The.NET Core HttpClient stack keeps its own buffers on top of the underlying outgoing connection socket.
570-
// We flush those buffers down to the socket on every write when this is set,
571-
// but it does NOT result in calls to flush on the underlying socket.
572-
// This is necessary because we proxy http2 transparently,
573-
// and we are deliberately unaware of packet structure used e.g. in gRPC duplex channels.
574-
// Because the sockets aren't flushed, the perf impact of this choice is expected to be small.
575-
// Future: It may be wise to set this to true for *all* http2 incoming requests,
576-
// but for now, out of an abundance of caution, we only do it for requests that look like gRPC.
577-
return new StreamCopyHttpContent(
578-
request: request,
579-
autoFlushHttpClientOutgoingStream: isStreamingRequest,
580-
timeProvider: _timeProvider,
581-
activityToken);
565+
return new StreamCopyHttpContent(context, isStreamingRequest, _timeProvider, _logger, activityToken);
582566
}
583567

584568
return null;
585569
}
586570

587-
private ForwarderError HandleRequestBodyFailure(HttpContext context, StreamCopyResult requestBodyCopyResult, Exception requestBodyException, Exception additionalException)
571+
private ForwarderError HandleRequestBodyFailure(HttpContext context, StreamCopyResult requestBodyCopyResult, Exception requestBodyException, Exception additionalException, bool timedOut)
588572
{
589573
ForwarderError requestBodyError;
590574
int statusCode;
@@ -593,19 +577,12 @@ private ForwarderError HandleRequestBodyFailure(HttpContext context, StreamCopyR
593577
// Failed while trying to copy the request body from the client. It's ambiguous if the request or response failed first.
594578
case StreamCopyResult.InputError:
595579
requestBodyError = ForwarderError.RequestBodyClient;
596-
statusCode = StatusCodes.Status400BadRequest;
580+
statusCode = timedOut ? StatusCodes.Status408RequestTimeout : StatusCodes.Status400BadRequest;
597581
break;
598582
// Failed while trying to copy the request body to the destination. It's ambiguous if the request or response failed first.
599583
case StreamCopyResult.OutputError:
600584
requestBodyError = ForwarderError.RequestBodyDestination;
601-
statusCode = StatusCodes.Status502BadGateway;
602-
break;
603-
// Canceled while trying to copy the request body, either due to a client disconnect or a timeout. This probably caused the response to fail as a secondary error.
604-
case StreamCopyResult.Canceled:
605-
requestBodyError = ForwarderError.RequestBodyCanceled;
606-
// Timeouts (504s) are handled at the SendAsync call site.
607-
// The request body should only be canceled by the RequestAborted token.
608-
statusCode = StatusCodes.Status502BadGateway;
585+
statusCode = timedOut ? StatusCodes.Status504GatewayTimeout : StatusCodes.Status502BadGateway;
609586
break;
610587
default:
611588
throw new NotImplementedException(requestBodyCopyResult.ToString());
@@ -630,33 +607,46 @@ private ForwarderError HandleRequestBodyFailure(HttpContext context, StreamCopyR
630607
private async ValueTask<ForwarderError> HandleRequestFailureAsync(HttpContext context, StreamCopyHttpContent? requestContent, Exception requestException,
631608
HttpTransformer transformer, ActivityCancellationTokenSource requestCancellationSource, bool failedDuringRequestCreation)
632609
{
633-
if (requestException is OperationCanceledException)
610+
var triedRequestBody = requestContent?.ConsumptionTask.IsCompleted == true;
611+
612+
if (requestCancellationSource.CancelledByLinkedToken)
634613
{
635-
if (requestCancellationSource.CancelledByLinkedToken)
614+
var requestBodyCanceled = false;
615+
if (triedRequestBody)
636616
{
637-
// Either the client went away (HttpContext.RequestAborted) or the CancellationToken provided to SendAsync was signaled.
638-
return await ReportErrorAsync(ForwarderError.RequestCanceled, StatusCodes.Status502BadGateway);
639-
}
640-
else
641-
{
642-
Debug.Assert(requestCancellationSource.IsCancellationRequested || requestException.ToString().Contains("ConnectTimeout"), requestException.ToString());
643-
return await ReportErrorAsync(ForwarderError.RequestTimedOut, StatusCodes.Status504GatewayTimeout);
617+
var (requestBodyCopyResult, requestBodyException) = requestContent!.ConsumptionTask.Result;
618+
requestBodyCanceled = requestBodyCopyResult == StreamCopyResult.Canceled;
619+
if (requestBodyCanceled)
620+
{
621+
requestException = new AggregateException(requestException, requestBodyException!);
622+
}
644623
}
624+
// Either the client went away (HttpContext.RequestAborted) or the CancellationToken provided to SendAsync was signaled.
625+
return await ReportErrorAsync(requestBodyCanceled ? ForwarderError.RequestBodyCanceled : ForwarderError.RequestCanceled,
626+
context.RequestAborted.IsCancellationRequested ? StatusCodes.Status400BadRequest : StatusCodes.Status502BadGateway);
645627
}
646628

647629
// Check for request body errors, these may have triggered the response error.
648-
if (requestContent?.ConsumptionTask.IsCompleted == true)
630+
if (triedRequestBody)
649631
{
650-
var (requestBodyCopyResult, requestBodyException) = requestContent.ConsumptionTask.Result;
632+
var (requestBodyCopyResult, requestBodyException) = requestContent!.ConsumptionTask.Result;
651633

652-
if (requestBodyCopyResult != StreamCopyResult.Success)
634+
if (requestBodyCopyResult is StreamCopyResult.InputError or StreamCopyResult.OutputError)
653635
{
654-
var error = HandleRequestBodyFailure(context, requestBodyCopyResult, requestBodyException!, requestException);
636+
var error = HandleRequestBodyFailure(context, requestBodyCopyResult, requestBodyException!, requestException,
637+
timedOut: requestCancellationSource.IsCancellationRequested);
655638
await transformer.TransformResponseAsync(context, proxyResponse: null, requestCancellationSource.Token);
656639
return error;
657640
}
658641
}
659642

643+
if (requestException is OperationCanceledException)
644+
{
645+
Debug.Assert(requestCancellationSource.IsCancellationRequested || requestException.ToString().Contains("ConnectTimeout"), requestException.ToString());
646+
647+
return await ReportErrorAsync(ForwarderError.RequestTimedOut, StatusCodes.Status504GatewayTimeout);
648+
}
649+
660650
// We couldn't communicate with the destination.
661651
return await ReportErrorAsync(failedDuringRequestCreation ? ForwarderError.RequestCreation : ForwarderError.Request, StatusCodes.Status502BadGateway);
662652

@@ -870,7 +860,7 @@ private ForwarderError FixupUpgradeResponseHeaders(HttpContext context, HttpResp
870860
return (StreamCopyResult.Success, null);
871861
}
872862

873-
private async ValueTask<ForwarderError> HandleResponseBodyErrorAsync(HttpContext context, StreamCopyHttpContent? requestContent, StreamCopyResult responseBodyCopyResult, Exception responseBodyException, CancellationTokenSource requestCancellationSource)
863+
private async ValueTask<ForwarderError> HandleResponseBodyErrorAsync(HttpContext context, StreamCopyHttpContent? requestContent, StreamCopyResult responseBodyCopyResult, Exception responseBodyException, ActivityCancellationTokenSource requestCancellationSource)
874864
{
875865
if (requestContent is not null && requestContent.Started)
876866
{
@@ -884,9 +874,10 @@ private async ValueTask<ForwarderError> HandleResponseBodyErrorAsync(HttpContext
884874
var (requestBodyCopyResult, requestBodyError) = await requestContent.ConsumptionTask;
885875

886876
// Check for request body errors, these may have triggered the response error.
887-
if (alreadyFinished && requestBodyCopyResult != StreamCopyResult.Success)
877+
if (alreadyFinished && requestBodyCopyResult is StreamCopyResult.InputError or StreamCopyResult.OutputError)
888878
{
889-
return HandleRequestBodyFailure(context, requestBodyCopyResult, requestBodyError!, responseBodyException);
879+
return HandleRequestBodyFailure(context, requestBodyCopyResult, requestBodyError!, responseBodyException,
880+
timedOut: requestCancellationSource.IsCancellationRequested && !requestCancellationSource.CancelledByLinkedToken);
890881
}
891882
}
892883

@@ -920,41 +911,6 @@ private static ValueTask CopyResponseTrailingHeadersAsync(HttpResponseMessage so
920911
return transformer.TransformResponseTrailersAsync(context, source, cancellationToken);
921912
}
922913

923-
924-
/// <summary>
925-
/// Disable some ASP .NET Core server limits so that we can handle long-running gRPC requests unconstrained.
926-
/// Note that the gRPC server implementation on ASP .NET Core does the same for client-streaming and duplex methods.
927-
/// Since in Gateway we have no way to determine if the current request requires client-streaming or duplex comm,
928-
/// we do this for *all* incoming requests that look like they might be gRPC.
929-
/// </summary>
930-
/// <remarks>
931-
/// Inspired on
932-
/// <see href="https://github.com/grpc/grpc-dotnet/blob/3ce9b104524a4929f5014c13cd99ba9a1c2431d4/src/Grpc.AspNetCore.Server/Internal/CallHandlers/ServerCallHandlerBase.cs#L127"/>.
933-
/// </remarks>
934-
private void DisableMinRequestBodyDataRateAndMaxRequestBodySize(HttpContext httpContext)
935-
{
936-
var minRequestBodyDataRateFeature = httpContext.Features.Get<IHttpMinRequestBodyDataRateFeature>();
937-
if (minRequestBodyDataRateFeature is not null)
938-
{
939-
minRequestBodyDataRateFeature.MinDataRate = null;
940-
}
941-
942-
var maxRequestBodySizeFeature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
943-
if (maxRequestBodySizeFeature is not null)
944-
{
945-
if (!maxRequestBodySizeFeature.IsReadOnly)
946-
{
947-
maxRequestBodySizeFeature.MaxRequestBodySize = null;
948-
}
949-
else
950-
{
951-
// IsReadOnly could be true if middleware has already started reading the request body
952-
// In that case we can't disable the max request body size for the request stream
953-
_logger.LogWarning("Unable to disable max request body size.");
954-
}
955-
}
956-
}
957-
958914
private void ReportProxyError(HttpContext context, ForwarderError error, Exception ex)
959915
{
960916
context.Features.Set<IForwarderErrorFeature>(new ForwarderErrorFeature(error, ex));

src/ReverseProxy/Forwarder/StreamCopier.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,13 @@ internal static class StreamCopier
124124
telemetry?.AfterWrite();
125125
}
126126

127-
var result = ex is OperationCanceledException ? StreamCopyResult.Canceled :
128-
(read == 0 ? StreamCopyResult.InputError : StreamCopyResult.OutputError);
127+
if (activityToken.CancelledByLinkedToken)
128+
{
129+
return (StreamCopyResult.Canceled, ex);
130+
}
129131

132+
// If the activity timeout triggered while reading or writing, blame the sender or receiver.
133+
var result = read == 0 ? StreamCopyResult.InputError : StreamCopyResult.OutputError;
130134
return (result, ex);
131135
}
132136
finally

0 commit comments

Comments
 (0)