@@ -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 ) ) ;
0 commit comments