private async ValueTask <ForwarderError> HandleUpgradedResponse(HttpContext context, HttpResponseMessage destinationResponse, ActivityCancellationTokenSource activityCancellationSource) { ForwarderTelemetry.Log.ForwarderStage(ForwarderStage.ResponseUpgrade); // SocketHttpHandler and similar transports always provide an HttpContent object, even if it's empty. // Note as of 5.0 HttpResponse.Content never returns null. // https://github.com/dotnet/runtime/blame/8fc68f626a11d646109a758cb0fc70a0aa7826f1/src/libraries/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs#L46 if (destinationResponse.Content == null) { throw new InvalidOperationException("A response content is required for upgrades."); } // :: Step 7-A-1: Upgrade the client channel. This will also send response headers. var upgradeFeature = context.Features.Get <IHttpUpgradeFeature>(); if (upgradeFeature == null) { var ex = new InvalidOperationException("Invalid 101 response when upgrades aren't supported."); destinationResponse.Dispose(); context.Response.StatusCode = StatusCodes.Status502BadGateway; ReportProxyError(context, ForwarderError.UpgradeResponseDestination, ex); return(ForwarderError.UpgradeResponseDestination); } RestoreUpgradeHeaders(context, destinationResponse); Stream upgradeResult; try { upgradeResult = await upgradeFeature.UpgradeAsync(); } catch (Exception ex) { destinationResponse.Dispose(); ReportProxyError(context, ForwarderError.UpgradeResponseClient, ex); return(ForwarderError.UpgradeResponseClient); } using var clientStream = upgradeResult; // :: Step 7-A-2: Copy duplex streams using var destinationStream = await destinationResponse.Content.ReadAsStreamAsync(); var requestTask = StreamCopier.CopyAsync(isRequest: true, clientStream, destinationStream, StreamCopier.UnknownLength, _clock, activityCancellationSource, activityCancellationSource.Token).AsTask(); var responseTask = StreamCopier.CopyAsync(isRequest: false, destinationStream, clientStream, StreamCopier.UnknownLength, _clock, activityCancellationSource, activityCancellationSource.Token).AsTask(); // Make sure we report the first failure. var firstTask = await Task.WhenAny(requestTask, responseTask); var requestFinishedFirst = firstTask == requestTask; var secondTask = requestFinishedFirst ? responseTask : requestTask; ForwarderError error; var(firstResult, firstException) = await firstTask; if (firstResult != StreamCopyResult.Success) { error = ReportResult(context, requestFinishedFirst, firstResult, firstException); // Cancel the other direction activityCancellationSource.Cancel(); // Wait for this to finish before exiting so the resources get cleaned up properly. await secondTask; } else { var(secondResult, secondException) = await secondTask; if (secondResult != StreamCopyResult.Success) { error = ReportResult(context, !requestFinishedFirst, secondResult, secondException !); } else { error = ForwarderError.None; } } return(error); ForwarderError ReportResult(HttpContext context, bool reqeuest, StreamCopyResult result, Exception exception) { var error = result switch { StreamCopyResult.InputError => reqeuest ? ForwarderError.UpgradeRequestClient : ForwarderError.UpgradeResponseDestination, StreamCopyResult.OutputError => reqeuest ? ForwarderError.UpgradeRequestDestination : ForwarderError.UpgradeResponseClient, StreamCopyResult.Canceled => reqeuest ? ForwarderError.UpgradeRequestCanceled : ForwarderError.UpgradeResponseCanceled, _ => throw new NotImplementedException(result.ToString()), }; ReportProxyError(context, error, exception); return(error); } }