private async ValueTask <(StreamCopyResult, Exception)> CopyResponseBodyAsync(HttpContent destinationResponseContent, Stream clientResponseStream, CancellationToken cancellation) { // SocketHttpHandler and similar transports always provide an HttpContent object, even if it's empty. // In 3.1 this is only likely to return null in tests. // 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 (destinationResponseContent != null) { using var destinationResponseStream = await destinationResponseContent.ReadAsStreamAsync(); return(await StreamCopier.CopyAsync(isRequest : false, destinationResponseStream, clientResponseStream, _clock, cancellation)); } return(StreamCopyResult.Success, null); }
private async Task HandleUpgradedResponse(HttpContext context, HttpResponseMessage destinationResponse, CancellationToken longCancellation) { ProxyTelemetry.Log.ProxyStage(ProxyStage.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>(); Stream upgradeResult; try { upgradeResult = await upgradeFeature.UpgradeAsync(); } catch (Exception ex) { destinationResponse.Dispose(); ReportProxyError(context, ProxyError.UpgradeResponseClient, ex); return; } using var clientStream = upgradeResult; // :: Step 7-A-2: Copy duplex streams using var destinationStream = await destinationResponse.Content.ReadAsStreamAsync(); using var abortTokenSource = CancellationTokenSource.CreateLinkedTokenSource(longCancellation); var requestTask = StreamCopier.CopyAsync(isRequest: true, clientStream, destinationStream, _clock, abortTokenSource.Token).AsTask(); var responseTask = StreamCopier.CopyAsync(isRequest: false, destinationStream, clientStream, _clock, abortTokenSource.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; var(firstResult, firstException) = await firstTask; if (firstResult != StreamCopyResult.Success) { ReportResult(context, requestFinishedFirst, firstResult, firstException); // Cancel the other direction abortTokenSource.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) { ReportResult(context, requestFinishedFirst, secondResult, secondException); } } void ReportResult(HttpContext context, bool reqeuest, StreamCopyResult result, Exception exception) { var error = result switch { StreamCopyResult.InputError => reqeuest ? ProxyError.UpgradeRequestClient : ProxyError.UpgradeResponseDestination, StreamCopyResult.OutputError => reqeuest ? ProxyError.UpgradeRequestDestination : ProxyError.UpgradeResponseClient, StreamCopyResult.Canceled => reqeuest ? ProxyError.UpgradeRequestCanceled : ProxyError.UpgradeResponseCanceled, _ => throw new NotImplementedException(result.ToString()), }; ReportProxyError(context, error, exception); } }