/// <summary> /// Copies bytes from the stream provided in our constructor into the target <paramref name="stream"/>. /// </summary> /// <remarks> /// This is used internally by HttpClient.SendAsync to send the request body. /// Here's the sequence of events as of commit 17300169760c61a90cab8d913636c1058a30a8c1 (https://github.com/dotnet/corefx -- tag v3.1.1). /// /// <code> /// HttpClient.SendAsync --> /// HttpMessageInvoker.SendAsync --> /// HttpClientHandler.SendAsync --> /// SocketsHttpHandler.SendAsync --> /// HttpConnectionHandler.SendAsync --> /// HttpConnectionPoolManager.SendAsync --> /// HttpConnectionPool.SendAsync --> ... --> /// { /// HTTP/1.1: HttpConnection.SendAsync --> /// HttpConnection.SendAsyncCore --> /// HttpConnection.SendRequestContentAsync --> /// HttpContent.CopyToAsync /// /// HTTP/2: Http2Connection.SendAsync --> /// Http2Stream.SendRequestBodyAsync --> /// HttpContent.CopyToAsync /// /// /* Only in .NET 5: /// HTTP/3: Http3Connection.SendAsync --> /// Http3Connection.SendWithoutWaitingAsync --> /// Http3RequestStream.SendAsync --> /// Http3RequestStream.SendContentAsync --> /// HttpContent.CopyToAsync /// */ /// } /// /// HttpContent.CopyToAsync --> /// HttpContent.SerializeToStreamAsync (bingo!) /// </code> /// /// Conclusion: by overriding HttpContent.SerializeToStreamAsync, /// we have full control over pumping bytes to the target stream for all protocols /// (except Web Sockets, which is handled separately). /// </remarks> protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) { if (Started) { throw new InvalidOperationException("Stream was already consumed."); } Started = true; try { if (_autoFlushHttpClientOutgoingStream) { // HttpClient's machinery keeps an internal buffer that doesn't get flushed to the socket on every write. // Some protocols (e.g. gRPC) may rely on specific bytes being sent, and HttpClient's buffering would prevent it. // AutoFlushingStream delegates to the provided stream, adding calls to FlushAsync on every WriteAsync. // Note that HttpClient does NOT call Flush on the underlying socket, so the perf impact of this is expected to be small. // This statement is based on current knowledge as of .NET Core 3.1.201. stream = new AutoFlushingStream(stream); } // Immediately flush request stream to send headers // https://github.com/dotnet/corefx/issues/39586#issuecomment-516210081 await stream.FlushAsync(); await _streamCopier.CopyAsync(_source, stream, _cancellation); _tcs.TrySetResult(true); } catch (Exception ex) { _tcs.TrySetException(ex); throw; } }
/// <summary> /// Proxies an upgradable request to the upstream server, treating the upgraded stream as an opaque duplex channel. /// </summary> /// <remarks> /// Upgradable request proxying comprises the following steps: /// (1) Create outgoing HttpRequestMessage /// (2) Copy request headers Downstream ---► Proxy ---► Upstream /// (3) Send the outgoing request using HttpMessageInvoker Downstream ---► Proxy ---► Upstream /// (4) Copy response status line Downstream ◄--- Proxy ◄--- Upstream /// (5) Copy response headers Downstream ◄--- Proxy ◄--- Upstream /// Scenario A: upgrade with upstream worked (got 101 response) /// (A-6) Upgrade downstream channel (also sends response headers) Downstream ◄--- Proxy ◄--- Upstream /// (A-7) Copy duplex streams Downstream ◄--► Proxy ◄--► Upstream /// ---- or ---- /// Scenario B: upgrade with upstream failed (got non-101 response) /// (B-6) Send response headers Downstream ◄--- Proxy ◄--- Upstream /// (B-7) Copy response body Downstream ◄--- Proxy ◄--- Upstream /// /// This takes care of WebSockets as well as any other upgradable protocol. /// </remarks> private async Task UpgradableProxyAsync( HttpContext context, IHttpUpgradeFeature upgradeFeature, HttpRequestMessage upstreamRequest, HttpMessageInvoker httpClient, CancellationToken shortCancellation, CancellationToken longCancellation, Action <HttpRequestMessage> requestAction = null) { _ = context ?? throw new ArgumentNullException(nameof(context)); _ = upgradeFeature ?? throw new ArgumentNullException(nameof(upgradeFeature)); _ = upstreamRequest ?? throw new ArgumentNullException(nameof(upstreamRequest)); _ = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 2: Copy request headers Downstream --► Proxy --► Upstream CopyHeadersToUpstream(context, upstreamRequest); if (requestAction != null) { requestAction(upstreamRequest); } // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 3: Send the outgoing request using HttpMessageInvoker var upstreamResponse = await httpClient.SendAsync(upstreamRequest, shortCancellation); var upgraded = upstreamResponse.StatusCode == HttpStatusCode.SwitchingProtocols && upstreamResponse.Content != null; // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 4: Copy response status line Downstream ◄-- Proxy ◄-- Upstream context.Response.StatusCode = (int)upstreamResponse.StatusCode; context.Features.Get <IHttpResponseFeature>().ReasonPhrase = upstreamResponse.ReasonPhrase; // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 5: Copy response headers Downstream ◄-- Proxy ◄-- Upstream CopyHeadersToDownstream(upstreamResponse, context); if (!upgraded) { // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step B-6: Send response headers Downstream ◄-- Proxy ◄-- Upstream // This is important to avoid any extra delays in sending response headers // e.g. if the upstream server is slow to provide its response body. await context.Response.StartAsync(shortCancellation); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step B-7: Copy response body Downstream ◄-- Proxy ◄-- Upstream await CopyBodyDownstreamAsync(upstreamResponse.Content, context.Response.Body, longCancellation); return; } // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step A-6: Upgrade the downstream channel. This will send all response headers too. using var downstreamStream = await upgradeFeature.UpgradeAsync(); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step A-7: Copy duplex streams var upstreamStream = await upstreamResponse.Content.ReadAsStreamAsync(); var upstreamCopier = new StreamCopier(); var upstreamTask = upstreamCopier.CopyAsync(downstreamStream, upstreamStream, longCancellation); var downstreamCopier = new StreamCopier(); var downstreamTask = downstreamCopier.CopyAsync(upstreamStream, downstreamStream, longCancellation); await Task.WhenAll(upstreamTask, downstreamTask); }