/// <summary> /// Proxies the incoming request to the upstream server, and the response back to our client. /// </summary> /// <remarks> /// In what follows, as well as throughout in Reverse Proxy, we consider /// the following picture as illustrative of the Proxy. /// <code> /// +-------------------+ /// | Upstream server + /// +-------------------+ /// ▲ | /// (b) | | (c) /// | ▼ /// +-------------------+ /// | Proxy + /// +-------------------+ /// ▲ | /// (a) | | (d) /// | ▼ /// +-------------------+ /// | Downstream client + /// +-------------------+ /// </code> /// /// (a) and (b) show the *request* path, going *upstream* from the client to the target. /// (c) and (d) show the *response* path, going *downstream* from the target back to the client. /// </remarks> /// <param name="longCancellation">This should be linked to a client disconnect notification like <see cref="HttpContext.RequestAborted"/> /// to avoid leaking long running requests.</param> public Task ProxyAsync( HttpContext context, string destinationPrefix, Transforms transforms, IProxyHttpClientFactory httpClientFactory, ProxyTelemetryContext proxyTelemetryContext, CancellationToken shortCancellation, CancellationToken longCancellation) { Contracts.CheckValue(context, nameof(context)); Contracts.CheckValue(destinationPrefix, nameof(destinationPrefix)); Contracts.CheckValue(transforms, nameof(transforms)); Contracts.CheckValue(httpClientFactory, nameof(httpClientFactory)); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 1: Create outgoing HttpRequestMessage var upgradeFeature = context.Features.Get <IHttpUpgradeFeature>(); var isUpgrade = upgradeFeature?.IsUpgradableRequest ?? false; // Default to HTTP/1.1 for proxying upgradable requests. This is already the default as of .NET Core 3.1 // Otherwise request HTTP/2 and let HttpClient fallback to HTTP/1.1 if it cannot establish HTTP/2 with the target. // This is done without extra round-trips thanks to ALPN. We can detect a downgrade after calling HttpClient.SendAsync // (see Step 3 below). TBD how this will change when HTTP/3 is supported. var version = isUpgrade ? ProtocolHelper.Http11Version : ProtocolHelper.Http2Version; var request = CreateRequestMessage(context, destinationPrefix, version, transforms.RequestTransforms); if (isUpgrade) { return(UpgradableProxyAsync(context, upgradeFeature, request, transforms, httpClientFactory.CreateUpgradableClient(), proxyTelemetryContext, shortCancellation, longCancellation)); } else { return(NormalProxyAsync(context, request, transforms, httpClientFactory.CreateNormalClient(), proxyTelemetryContext, shortCancellation, longCancellation)); } }
/// <summary> /// Proxies the incoming request to the upstream server, and the response back to our client. /// </summary> /// <remarks> /// In what follows, as well as throughout in Reverse Proxy, we consider /// the following picture as illustrative of the Proxy. /// <code> /// +-------------------+ /// | Upstream server + /// +-------------------+ /// ▲ | /// (b) | | (c) /// | ▼ /// +-------------------+ /// | Proxy + /// +-------------------+ /// ▲ | /// (a) | | (d) /// | ▼ /// +-------------------+ /// | Downstream client + /// +-------------------+ /// </code> /// /// (a) and (b) show the *request* path, going *upstream* from the client to the target. /// (c) and (d) show the *response* path, going *downstream* from the target back to the client. /// </remarks> /// <param name="longCancellation">This should be linked to a client disconnect notification like <see cref="HttpContext.RequestAborted"/> /// to avoid leaking long running requests.</param> public Task ProxyAsync( HttpContext context, Uri targetUri, IProxyHttpClientFactory httpClientFactory, ProxyTelemetryContext proxyTelemetryContext, CancellationToken shortCancellation, CancellationToken longCancellation) { Contracts.CheckValue(context, nameof(context)); Contracts.CheckValue(targetUri, nameof(targetUri)); Contracts.CheckValue(httpClientFactory, nameof(httpClientFactory)); var upgradeFeature = context.Features.Get <IHttpUpgradeFeature>(); if (upgradeFeature == null || !upgradeFeature.IsUpgradableRequest) { return(NormalProxyAsync(context, targetUri, httpClientFactory.CreateNormalClient(), proxyTelemetryContext, shortCancellation, longCancellation)); } else { return(UpgradableProxyAsync(context, upgradeFeature, targetUri, httpClientFactory.CreateUpgradableClient(), proxyTelemetryContext, shortCancellation, longCancellation)); } }
/// <summary> /// Proxies the incoming request to the upstream server, and the response back to our client. /// </summary> /// <remarks> /// In what follows, as well as throughout in Reverse Proxy, we consider /// the following picture as illustrative of the Proxy. /// <code> /// +-------------------+ /// | Upstream server + /// +-------------------+ /// ▲ | /// (b) | | (c) /// | ▼ /// +-------------------+ /// | Proxy + /// +-------------------+ /// ▲ | /// (a) | | (d) /// | ▼ /// +-------------------+ /// | Downstream client + /// +-------------------+ /// </code> /// /// (a) and (b) show the *request* path, going *upstream* from the client to the target. /// (c) and (d) show the *response* path, going *downstream* from the target back to the client. /// </remarks> /// <param name="longCancellation">This should be linked to a client disconnect notification like <see cref="HttpContext.RequestAborted"/> /// to avoid leaking long running requests.</param> public Task ProxyAsync( HttpContext context, string destinationPrefix, Transforms transforms, IProxyHttpClientFactory httpClientFactory, ProxyTelemetryContext proxyTelemetryContext, CancellationToken shortCancellation, CancellationToken longCancellation) { _ = context ?? throw new ArgumentNullException(nameof(context)); _ = destinationPrefix ?? throw new ArgumentNullException(nameof(destinationPrefix)); _ = transforms ?? throw new ArgumentNullException(nameof(transforms)); _ = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 1: Create outgoing HttpRequestMessage var upgradeFeature = context.Features.Get <IHttpUpgradeFeature>(); var isUpgrade = (upgradeFeature?.IsUpgradableRequest ?? false) // Mitigate https://github.com/microsoft/reverse-proxy/issues/255, IIS considers all requests upgradeable. && string.Equals("WebSocket", context.Request.Headers[HeaderNames.Upgrade], StringComparison.OrdinalIgnoreCase); // Default to HTTP/1.1 for proxying upgradeable requests. This is already the default as of .NET Core 3.1 // Otherwise request HTTP/2 and let HttpClient fallback to HTTP/1.1 if it cannot establish HTTP/2 with the target. // This is done without extra round-trips thanks to ALPN. We can detect a downgrade after calling HttpClient.SendAsync // (see Step 3 below). TBD how this will change when HTTP/3 is supported. var version = isUpgrade ? ProtocolHelper.Http11Version : ProtocolHelper.Http2Version; var request = CreateRequestMessage(context, destinationPrefix, version, transforms.RequestTransforms); if (isUpgrade) { return(UpgradableProxyAsync(context, upgradeFeature, request, transforms, httpClientFactory.CreateClient(), proxyTelemetryContext, shortCancellation, longCancellation)); } else { return(NormalProxyAsync(context, request, transforms, httpClientFactory.CreateClient(), proxyTelemetryContext, shortCancellation, longCancellation)); } }
/// <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, Uri targetUri, HttpMessageInvoker httpClient, ProxyTelemetryContext proxyTelemetryContext, CancellationToken shortCancellation, CancellationToken longCancellation) { Contracts.CheckValue(context, nameof(context)); Contracts.CheckValue(upgradeFeature, nameof(upgradeFeature)); Contracts.CheckValue(targetUri, nameof(targetUri)); Contracts.CheckValue(httpClient, nameof(httpClient)); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 1: Create outgoing HttpRequestMessage var upstreamRequest = new HttpRequestMessage(HttpUtilities.GetHttpMethod(context.Request.Method), targetUri) { // Default to HTTP/1.1 for proxying upgradable requests. This is already the default as of .NET Core 3.1 Version = new Version(1, 1), }; // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 2: Copy request headers Downstream --► Proxy --► Upstream CopyHeadersToUpstream(context, 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.Response.Headers); 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, proxyTelemetryContext, 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( _metrics, new StreamCopyTelemetryContext( direction: "upstream", backendId: proxyTelemetryContext.BackendId, routeId: proxyTelemetryContext.RouteId, destinationId: proxyTelemetryContext.DestinationId)); var upstreamTask = upstreamCopier.CopyAsync(downstreamStream, upstreamStream, longCancellation); var downstreamCopier = new StreamCopier( _metrics, new StreamCopyTelemetryContext( direction: "downstream", backendId: proxyTelemetryContext.BackendId, routeId: proxyTelemetryContext.RouteId, destinationId: proxyTelemetryContext.DestinationId)); var downstreamTask = downstreamCopier.CopyAsync(upstreamStream, downstreamStream, longCancellation); await Task.WhenAll(upstreamTask, downstreamTask); }
/// <summary> /// Proxies a normal (i.e. non-upgradable) request to the upstream server, and the response back to our client. /// </summary> /// <remarks> /// Normal proxying comprises the following steps: /// (0) Disable ASP .NET Core limits for streaming requests /// (1) Create outgoing HttpRequestMessage /// (2) Setup copy of request body (background) Downstream --► Proxy --► Upstream /// (3) Copy request headers Downstream --► Proxy --► Upstream /// (4) Send the outgoing request using HttpMessageInvoker Downstream --► Proxy --► Upstream /// (5) Copy response status line Downstream ◄-- Proxy ◄-- Upstream /// (6) Copy response headers Downstream ◄-- Proxy ◄-- Upstream /// (7) Copy response body Downstream ◄-- Proxy ◄-- Upstream /// (8) Copy response trailer headers and finish response Downstream ◄-- Proxy ◄-- Upstream /// (9) Wait for completion of step 2: copying request body Downstream --► Proxy --► Upstream /// /// ASP .NET Core (Kestrel) will finally send response trailers (if any) /// after we complete the steps above and relinquish control. /// </remarks> private async Task NormalProxyAsync( HttpContext context, Uri targetUri, HttpMessageInvoker httpClient, ProxyTelemetryContext proxyTelemetryContext, CancellationToken shortCancellation, CancellationToken longCancellation) { Contracts.CheckValue(context, nameof(context)); Contracts.CheckValue(targetUri, nameof(targetUri)); Contracts.CheckValue(httpClient, nameof(httpClient)); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 0: Disable ASP .NET Core limits for streaming requests var isIncomingHttp2 = ProtocolHelper.IsHttp2(context.Request.Protocol); // NOTE: We heuristically assume gRPC-looking requests may require streaming semantics. // See https://github.com/microsoft/reverse-proxy/issues/118 for design discussion. var isStreamingRequest = isIncomingHttp2 && ProtocolHelper.IsGrpcContentType(context.Request.ContentType); if (isStreamingRequest) { DisableMinRequestBodyDataRateAndMaxRequestBodySize(context); } // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 1: Create outgoing HttpRequestMessage var upstreamRequest = new HttpRequestMessage(HttpUtilities.GetHttpMethod(context.Request.Method), targetUri) { // We request HTTP/2, but HttpClient will fallback to HTTP/1.1 if it cannot establish HTTP/2 with the target. // This is done without extra round-trips thanks to ALPN. We can detect a downgrade after calling HttpClient.SendAsync // (see Step 3 below). TBD how this will change when HTTP/3 is supported. Version = Http2Version, }; // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 2: Setup copy of request body (background) Downstream --► Proxy --► Upstream // Note that we must do this before step (3) because step (3) may also add headers to the HttpContent that we set up here. var bodyToUpstreamContent = SetupCopyBodyUpstream(context.Request.Body, upstreamRequest, in proxyTelemetryContext, isStreamingRequest, longCancellation); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 3: Copy request headers Downstream --► Proxy --► Upstream CopyHeadersToUpstream(context, upstreamRequest); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 4: Send the outgoing request using HttpClient ////this.logger.LogInformation($" Starting Proxy --> upstream request"); var upstreamResponse = await httpClient.SendAsync(upstreamRequest, shortCancellation); // Detect connection downgrade, which may be problematic for e.g. gRPC. if (isIncomingHttp2 && upstreamResponse.Version.Major != 2) { // TODO: Do something on connection downgrade... Log.HttpDowngradeDeteced(_logger); } // Assert that, if we are proxying content upstream, it must have started by now // (since HttpClient.SendAsync has already completed asynchronously). // If this check fails, there is a coding defect which would otherwise // cause us to wait forever in step 9, so fail fast here. if (bodyToUpstreamContent != null && !bodyToUpstreamContent.Started) { // TODO: bodyToUpstreamContent is never null. HttpClient might would not need to read the body in some scenarios, such as an early auth failure with Expect: 100-continue. throw new InvalidOperationException("Proxying the downstream request body to the upstream server hasn't started. This is a coding defect."); } // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 5: Copy response status line Downstream ◄-- Proxy ◄-- Upstream ////this.logger.LogInformation($" Setting downstream <-- Proxy status: {(int)upstreamResponse.StatusCode} {upstreamResponse.ReasonPhrase}"); context.Response.StatusCode = (int)upstreamResponse.StatusCode; context.Features.Get <IHttpResponseFeature>().ReasonPhrase = upstreamResponse.ReasonPhrase; // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 6: Copy response headers Downstream ◄-- Proxy ◄-- Upstream CopyHeadersToDownstream(upstreamResponse, context.Response.Headers); // NOTE: it may *seem* wise to call `context.Response.StartAsync()` at this point // since it looks like we are ready to send back response headers // (and this might help reduce extra delays while we wait to receive the body from upstream). // HOWEVER, this would produce the wrong result if it turns out that there is no content // from the upstream -- instead of sending headers and terminating the stream at once, // we would send headers thinking a body may be coming, and there is none. // This is problematic on gRPC connections when the upstream server encounters an error, // in which case it immediately returns the response headers and trailing headers, but no content, // and clients misbehave if the initial headers response does not indicate stream end. // TODO: Some of the tasks in steps (7) - (9) may go unobserved depending on what fails first. Needs more consideration. // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 7: Copy response body Downstream ◄-- Proxy ◄-- Upstream await CopyBodyDownstreamAsync(upstreamResponse.Content, context.Response.Body, proxyTelemetryContext, longCancellation); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 8: Copy response trailer headers and finish response Downstream ◄-- Proxy ◄-- Upstream CopyTrailingHeadersToDownstream(upstreamResponse, context); if (isStreamingRequest) { // NOTE: We must call `CompleteAsync` so that Kestrel will flush all bytes to the client. // In the case where there was no response body, // this is also when headers and trailing headers are sent to the client. // Without this, the client might wait forever waiting for response bytes, // while we might wait forever waiting for request bytes, // leading to a stuck connection and no way to make progress. await context.Response.CompleteAsync(); } // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 9: Wait for completion of step 2: copying request body Downstream --► Proxy --► Upstream if (bodyToUpstreamContent != null) { ////this.logger.LogInformation($" Waiting for downstream --> Proxy --> upstream body proxying to complete"); await bodyToUpstreamContent.ConsumptionTask; } }
/// <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, Transforms transforms, HttpMessageInvoker httpClient, ProxyTelemetryContext proxyTelemetryContext, CancellationToken shortCancellation, CancellationToken longCancellation) { _ = 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, transforms.CopyRequestHeaders, transforms.RequestHeaderTransforms); // ::::::::::::::::::::::::::::::::::::::::::::: // :: 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, transforms.ResponseHeaderTransforms); 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, proxyTelemetryContext, 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( _metrics, new StreamCopyTelemetryContext( direction: "upstream", clusterId: proxyTelemetryContext.ClusterId, routeId: proxyTelemetryContext.RouteId, destinationId: proxyTelemetryContext.DestinationId)); var upstreamTask = upstreamCopier.CopyAsync(downstreamStream, upstreamStream, longCancellation); var downstreamCopier = new StreamCopier( _metrics, new StreamCopyTelemetryContext( direction: "downstream", clusterId: proxyTelemetryContext.ClusterId, routeId: proxyTelemetryContext.RouteId, destinationId: proxyTelemetryContext.DestinationId)); var downstreamTask = downstreamCopier.CopyAsync(upstreamStream, downstreamStream, longCancellation); await Task.WhenAll(upstreamTask, downstreamTask); }
/// <summary> /// Proxies the incoming request to the destination server, and the response back to our client. /// </summary> /// <remarks> /// In what follows, as well as throughout in Reverse Proxy, we consider /// the following picture as illustrative of the Proxy. /// <code> /// +-------------------+ /// | Destination + /// +-------------------+ /// ▲ | /// (b) | | (c) /// | ▼ /// +-------------------+ /// | Proxy + /// +-------------------+ /// ▲ | /// (a) | | (d) /// | ▼ /// +-------------------+ /// | Client + /// +-------------------+ /// </code> /// /// (a) and (b) show the *request* path, going from the client to the target. /// (c) and (d) show the *response* path, going from the destination back to the client. /// /// Normal proxying comprises the following steps: /// (0) Disable ASP .NET Core limits for streaming requests /// (1) Create outgoing HttpRequestMessage /// (2) Setup copy of request body (background) Client --► Proxy --► Destination /// (3) Copy request headers Client --► Proxy --► Destination /// (4) Send the outgoing request using HttpMessageInvoker Client --► Proxy --► Destination /// (5) Copy response status line Client ◄-- Proxy ◄-- Destination /// (6) Copy response headers Client ◄-- Proxy ◄-- Destination /// (7-A) Check for a 101 upgrade response, this takes care of WebSockets as well as any other upgradeable protocol. /// (7-A-1) Upgrade client channel Client ◄--- Proxy ◄--- Destination /// (7-A-2) Copy duplex streams and return Client ◄--► Proxy ◄--► Destination /// (7-B) Copy (normal) response body Client ◄-- Proxy ◄-- Destination /// (8) Copy response trailer headers and finish response Client ◄-- Proxy ◄-- Destination /// (9) Wait for completion of step 2: copying request body Client --► Proxy --► Destination /// /// ASP .NET Core (Kestrel) will finally send response trailers (if any) /// after we complete the steps above and relinquish control. /// </remarks> /// <param name="longCancellation">This should be linked to a client disconnect notification like <see cref="HttpContext.RequestAborted"/> /// to avoid leaking long running requests.</param> public async Task ProxyAsync( HttpContext context, string destinationPrefix, HttpMessageInvoker httpClient, RequestProxyOptions proxyOptions, ProxyTelemetryContext proxyTelemetryContext) { _ = context ?? throw new ArgumentNullException(nameof(context)); _ = destinationPrefix ?? throw new ArgumentNullException(nameof(destinationPrefix)); _ = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _ = proxyOptions ?? throw new ArgumentNullException(nameof(proxyOptions)); var requestAborted = context.RequestAborted; // :: Step 1: Create outgoing HttpRequestMessage var upgradeFeature = context.Features.Get <IHttpUpgradeFeature>(); var isUpgradeRequest = (upgradeFeature?.IsUpgradableRequest ?? false) // Mitigate https://github.com/microsoft/reverse-proxy/issues/255, IIS considers all requests upgradeable. && string.Equals("WebSocket", context.Request.Headers[HeaderNames.Upgrade], StringComparison.OrdinalIgnoreCase); var destinationRequest = CreateRequestMessage(context, destinationPrefix, isUpgradeRequest, proxyOptions.Transforms.RequestTransforms); var isClientHttp2 = ProtocolHelper.IsHttp2(context.Request.Protocol); // NOTE: We heuristically assume gRPC-looking requests may require streaming semantics. // See https://github.com/microsoft/reverse-proxy/issues/118 for design discussion. var isStreamingRequest = isClientHttp2 && ProtocolHelper.IsGrpcContentType(context.Request.ContentType); // :: Step 2: Setup copy of request body (background) Client --► Proxy --► Destination // Note that we must do this before step (3) because step (3) may also add headers to the HttpContent that we set up here. var requestContent = SetupRequestBodyCopy(context.Request, destinationRequest, in proxyTelemetryContext, isStreamingRequest, requestAborted); // :: Step 3: Copy request headers Client --► Proxy --► Destination CopyRequestHeaders(context, destinationRequest, proxyOptions.Transforms); // :: Step 4: Send the outgoing request using HttpClient HttpResponseMessage destinationResponse; var requestTimeoutSource = CancellationTokenSource.CreateLinkedTokenSource(requestAborted); requestTimeoutSource.CancelAfter(proxyOptions.RequestTimeout); var requestTimeoutToken = requestTimeoutSource.Token; try { destinationResponse = await httpClient.SendAsync(destinationRequest, requestTimeoutToken); } catch (OperationCanceledException canceledException) { if (!requestAborted.IsCancellationRequested && requestTimeoutToken.IsCancellationRequested) { ReportProxyError(context, ProxyError.RequestTimedOut, canceledException); context.Response.StatusCode = StatusCodes.Status504GatewayTimeout; return; } ReportProxyError(context, ProxyError.RequestCanceled, canceledException); context.Response.StatusCode = StatusCodes.Status502BadGateway; return; } catch (Exception requestException) { await HandleRequestFailureAsync(context, requestContent, requestException); return; } finally { requestTimeoutSource.Dispose(); } // Detect connection downgrade, which may be problematic for e.g. gRPC. if (isClientHttp2 && destinationResponse.Version.Major != 2) { // TODO: Do something on connection downgrade... Log.HttpDowngradeDetected(_logger); } // Assert that, if we are proxying content to the destination, it must have started by now // (since HttpClient.SendAsync has already completed asynchronously). // If this check fails, there is a coding defect which would otherwise // cause us to wait forever in step 9, so fail fast here. if (requestContent != null && !requestContent.Started) { // TODO: HttpClient might not need to read the body in some scenarios, such as an early auth failure with Expect: 100-continue. throw new InvalidOperationException("Proxying the Client request body to the Destination server hasn't started. This is a coding defect."); } // :: Step 5: Copy response status line Client ◄-- Proxy ◄-- Destination // :: Step 6: Copy response headers Client ◄-- Proxy ◄-- Destination CopyResponseStatusAndHeaders(destinationResponse, context, proxyOptions.Transforms.ResponseHeaderTransforms); // :: Step 7-A: Check for a 101 upgrade response, this takes care of WebSockets as well as any other upgradeable protocol. if (destinationResponse.StatusCode == HttpStatusCode.SwitchingProtocols) { await HandleUpgradedResponse(context, upgradeFeature, destinationResponse, proxyTelemetryContext, requestAborted); return; } // NOTE: it may *seem* wise to call `context.Response.StartAsync()` at this point // since it looks like we are ready to send back response headers // (and this might help reduce extra delays while we wait to receive the body from the destination). // HOWEVER, this would produce the wrong result if it turns out that there is no content // from the destination -- instead of sending headers and terminating the stream at once, // we would send headers thinking a body may be coming, and there is none. // This is problematic on gRPC connections when the destination server encounters an error, // in which case it immediately returns the response headers and trailing headers, but no content, // and clients misbehave if the initial headers response does not indicate stream end. // :: Step 7-B: Copy response body Client ◄-- Proxy ◄-- Destination var(responseBodyCopyResult, responseBodyException) = await CopyResponseBodyAsync(destinationResponse.Content, context.Response.Body, proxyTelemetryContext, requestAborted); if (responseBodyCopyResult != StreamCopyResult.Success) { await HandleResponseBodyErrorAsync(context, requestContent, responseBodyCopyResult, responseBodyException); return; } // :: Step 8: Copy response trailer headers and finish response Client ◄-- Proxy ◄-- Destination CopyResponseTrailingHeaders(destinationResponse, context, proxyOptions.Transforms.ResponseTrailerTransforms); if (isStreamingRequest) { // NOTE: We must call `CompleteAsync` so that Kestrel will flush all bytes to the client. // In the case where there was no response body, // this is also when headers and trailing headers are sent to the client. // Without this, the client might wait forever waiting for response bytes, // while we might wait forever waiting for request bytes, // leading to a stuck connection and no way to make progress. await context.Response.CompleteAsync(); } // :: Step 9: Wait for completion of step 2: copying request body Client --► Proxy --► Destination if (requestContent != null) { var(requestBodyCopyResult, requestBodyException) = await requestContent.ConsumptionTask; if (requestBodyCopyResult != StreamCopyResult.Success) { // The response succeeded. If there was a request body error then it was probably because the client or destination decided // to cancel it. Report as low severity. var error = requestBodyCopyResult switch { StreamCopyResult.InputError => ProxyError.RequestBodyClient, StreamCopyResult.OutputError => ProxyError.RequestBodyDestination, StreamCopyResult.Canceled => ProxyError.RequestBodyCanceled, _ => throw new NotImplementedException(requestBodyCopyResult.ToString()) }; ReportProxyError(context, error, requestBodyException); } } }