public async Task DestinationThrows_Reported(bool isRequest) { var events = TestEventListener.Collect(); const int SourceSize = 10; const int BytesPerRead = 3; var clock = new ManualClock(); var sourceWaitTime = TimeSpan.FromMilliseconds(12345); var destinationWaitTime = TimeSpan.FromMilliseconds(42); var source = new SlowStream(new MemoryStream(new byte[SourceSize]), clock, sourceWaitTime) { MaxBytesPerRead = BytesPerRead }; var destination = new SlowStream(new ThrowStream(), clock, destinationWaitTime); using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); var(result, error) = await StreamCopier.CopyAsync(isRequest, source, destination, SourceSize, clock, cts, cts.Token); Assert.Equal(StreamCopyResult.OutputError, result); Assert.IsAssignableFrom <IOException>(error); AssertContentTransferred(events, isRequest, contentLength: BytesPerRead, iops: 1, firstReadTime: sourceWaitTime, readTime: sourceWaitTime, writeTime: destinationWaitTime); }
public async Task SerializeToStreamAsync_RespectsContentCancellation() { var tcs = new TaskCompletionSource <byte>(TaskCreationOptions.RunContinuationsAsynchronously); var source = new ReadDelegatingStream(new MemoryStream(), async(buffer, cancellation) => { if (buffer.Length == 0) { return(0); } Assert.False(cancellation.IsCancellationRequested); await tcs.Task; Assert.True(cancellation.IsCancellationRequested); return(0); }); using var contentCts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); var sut = CreateContent(source, contentCancellation: contentCts); var copyToTask = sut.CopyToWithCancellationAsync(new MemoryStream()); contentCts.Cancel(); tcs.SetResult(0); await copyToTask; }
public async Task SlowStreams_TelemetryReportsCorrectTime(bool isRequest) { var events = TestEventListener.Collect(); const int SourceSize = 3; var sourceBytes = new byte[SourceSize]; var source = new MemoryStream(sourceBytes); var destination = new MemoryStream(); var clock = new ManualClock(); var sourceWaitTime = TimeSpan.FromMilliseconds(12345); var destinationWaitTime = TimeSpan.FromMilliseconds(42); using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); await StreamCopier.CopyAsync( isRequest, new SlowStream(source, clock, sourceWaitTime), new SlowStream(destination, clock, destinationWaitTime), SourceSize, clock, cts, cts.Token); Assert.Equal(sourceBytes, destination.ToArray()); AssertContentTransferred(events, isRequest, SourceSize, iops: SourceSize + 1, firstReadTime: sourceWaitTime, readTime: (SourceSize + 1) * sourceWaitTime, writeTime: SourceSize * destinationWaitTime); }
private static StreamCopyHttpContent CreateContent(Stream source = null, bool autoFlushHttpClientOutgoingStream = false, IClock clock = null, ActivityCancellationTokenSource contentCancellation = null) { source ??= new MemoryStream(); clock ??= new Clock(); contentCancellation ??= ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); return(new StreamCopyHttpContent(source, autoFlushHttpClientOutgoingStream, clock, contentCancellation)); }
private static StreamCopyHttpContent CreateContent(HttpRequest request = null, bool autoFlushHttpClientOutgoingStream = false, IClock clock = null, ActivityCancellationTokenSource contentCancellation = null) { request ??= new DefaultHttpContext().Request; clock ??= new Clock(); contentCancellation ??= ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); return(new StreamCopyHttpContent(request, autoFlushHttpClientOutgoingStream, clock, contentCancellation)); }
public void ActivityCancellationTokenSource_RespectsLinkedToken() { var linkedCts = new CancellationTokenSource(); var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), linkedCts.Token); linkedCts.Cancel(); Assert.True(cts.IsCancellationRequested); }
public void ActivityCancellationTokenSource_DoesNotPoolsCanceledSources() { var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); cts.Cancel(); var cts2 = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); Assert.NotSame(cts, cts2); }
public void ActivityCancellationTokenSource_ClearsRegistrations() { var linkedCts = new CancellationTokenSource(); var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), linkedCts.Token); cts.Return(); linkedCts.Cancel(); Assert.False(cts.IsCancellationRequested); }
public async Task ActivityCancellationTokenSource_RespectsTimeout() { var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromMilliseconds(1), CancellationToken.None); for (var i = 0; i < 1000; i++) { if (cts.IsCancellationRequested) { return; } await Task.Delay(1); } Assert.True(false, "Cts was not canceled"); }
public async Task CopyAsync_Works(bool isRequest) { var events = TestEventListener.Collect(); const int SourceSize = (128 * 1024) - 3; var sourceBytes = Enumerable.Range(0, SourceSize).Select(i => (byte)(i % 256)).ToArray(); var source = new MemoryStream(sourceBytes); var destination = new MemoryStream(); using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); await StreamCopier.CopyAsync(isRequest, source, destination, SourceSize, new Clock(), cts, cts.Token); Assert.False(cts.Token.IsCancellationRequested); Assert.Equal(sourceBytes, destination.ToArray()); AssertContentTransferred(events, isRequest, SourceSize); }
public void ActivityCancellationTokenSource_PoolsSources() { // This test can run in parallel with others making use of ActivityCancellationTokenSource // A different thread could have already added/removed a source from the queue for (var i = 0; i < 1000; i++) { var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); cts.Return(); var cts2 = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); cts2.Return(); if (ReferenceEquals(cts, cts2)) { return; } } Assert.True(false, "CancellationTokenSources were not pooled"); }
public async Task Cancelled_Reported(bool isRequest) { var events = TestEventListener.Collect(); var source = new MemoryStream(new byte[10]); var destination = new MemoryStream(); using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); cts.Cancel(); var(result, error) = await StreamCopier.CopyAsync(isRequest, source, destination, StreamCopier.UnknownLength, new ManualClock(), cts, cts.Token); Assert.Equal(StreamCopyResult.Canceled, result); Assert.IsAssignableFrom <OperationCanceledException>(error); AssertContentTransferred(events, isRequest, contentLength: 0, iops: 1, firstReadTime: TimeSpan.Zero, readTime: TimeSpan.Zero, writeTime: TimeSpan.Zero); }
public async Task SourceThrows_Reported(bool isRequest) { var events = TestEventListener.Collect(); var clock = new ManualClock(); var sourceWaitTime = TimeSpan.FromMilliseconds(12345); var source = new SlowStream(new ThrowStream(), clock, sourceWaitTime); var destination = new MemoryStream(); using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); var(result, error) = await StreamCopier.CopyAsync(isRequest, source, destination, StreamCopier.UnknownLength, clock, cts, cts.Token); Assert.Equal(StreamCopyResult.InputError, result); Assert.IsAssignableFrom <IOException>(error); AssertContentTransferred(events, isRequest, contentLength: 0, iops: 1, firstReadTime: sourceWaitTime, readTime: sourceWaitTime, writeTime: TimeSpan.Zero); }
/// <summary> /// Proxies the incoming request to the destination server, and the response back to the 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> public async ValueTask <ForwarderError> SendAsync( HttpContext context, string destinationPrefix, HttpMessageInvoker httpClient, ForwarderRequestConfig requestConfig, HttpTransformer transformer) { _ = context ?? throw new ArgumentNullException(nameof(context)); _ = destinationPrefix ?? throw new ArgumentNullException(nameof(destinationPrefix)); _ = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _ = requestConfig ?? throw new ArgumentNullException(nameof(requestConfig)); _ = transformer ?? throw new ArgumentNullException(nameof(transformer)); // HttpClient overload for SendAsync changes response behavior to fully buffered which impacts performance // See discussion in https://github.com/microsoft/reverse-proxy/issues/458 if (httpClient is HttpClient) { throw new ArgumentException($"The http client must be of type HttpMessageInvoker, not HttpClient", nameof(httpClient)); } ForwarderTelemetry.Log.ForwarderStart(destinationPrefix); var activityCancellationSource = ActivityCancellationTokenSource.Rent(requestConfig?.ActivityTimeout ?? DefaultTimeout, context.RequestAborted); try { 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 1-3: Create outgoing HttpRequestMessage var(destinationRequest, requestContent) = await CreateRequestMessageAsync( context, destinationPrefix, transformer, requestConfig, isStreamingRequest, activityCancellationSource); // :: Step 4: Send the outgoing request using HttpClient HttpResponseMessage destinationResponse; try { ForwarderTelemetry.Log.ForwarderStage(ForwarderStage.SendAsyncStart); destinationResponse = await httpClient.SendAsync(destinationRequest, activityCancellationSource.Token); ForwarderTelemetry.Log.ForwarderStage(ForwarderStage.SendAsyncStop); // Reset the timeout since we received the response headers. activityCancellationSource.ResetTimeout(); } catch (Exception requestException) { return(await HandleRequestFailureAsync(context, requestContent, requestException, transformer, activityCancellationSource)); } // 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); } try { // :: Step 5: Copy response status line Client ◄-- Proxy ◄-- Destination // :: Step 6: Copy response headers Client ◄-- Proxy ◄-- Destination var copyBody = await CopyResponseStatusAndHeadersAsync(destinationResponse, context, transformer); if (!copyBody) { // The transforms callback decided that the response body should be discarded. destinationResponse.Dispose(); if (requestContent is not null && requestContent.InProgress) { activityCancellationSource.Cancel(); await requestContent.ConsumptionTask; } return(ForwarderError.None); } } catch (Exception ex) { destinationResponse.Dispose(); if (requestContent is not null && requestContent.InProgress) { activityCancellationSource.Cancel(); await requestContent.ConsumptionTask; } ReportProxyError(context, ForwarderError.ResponseHeaders, ex); // Clear the response since status code, reason and some headers might have already been copied and we want clean 502 response. context.Response.Clear(); context.Response.StatusCode = StatusCodes.Status502BadGateway; return(ForwarderError.ResponseHeaders); } // :: 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) { Debug.Assert(requestContent?.Started != true); return(await HandleUpgradedResponse(context, destinationResponse, activityCancellationSource)); } // 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, activityCancellationSource); if (responseBodyCopyResult != StreamCopyResult.Success) { return(await HandleResponseBodyErrorAsync(context, requestContent, responseBodyCopyResult, responseBodyException !, activityCancellationSource)); } // :: Step 8: Copy response trailer headers and finish response Client ◄-- Proxy ◄-- Destination await CopyResponseTrailingHeadersAsync(destinationResponse, context, transformer); 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 // NOTE: It is possible for the request body to NOT be copied even when there was an incoming requet body, // e.g. when the request includes header `Expect: 100-continue` and the destination produced a non-1xx response. // We must only wait for the request body to complete if it actually started, // otherwise we run the risk of waiting indefinitely for a task that will never complete. if (requestContent is not null && requestContent.Started) { 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 => ForwarderError.RequestBodyClient, StreamCopyResult.OutputError => ForwarderError.RequestBodyDestination, StreamCopyResult.Canceled => ForwarderError.RequestBodyCanceled, _ => throw new NotImplementedException(requestBodyCopyResult.ToString()) }; ReportProxyError(context, error, requestBodyException !); return(error); } } } finally { activityCancellationSource.Return(); ForwarderTelemetry.Log.ForwarderStop(context.Response.StatusCode); } return(ForwarderError.None); }
public async Task LongContentTransfer_TelemetryReportsTransferringEvents(bool isRequest) { var events = TestEventListener.Collect(); const int SourceSize = 123; var sourceBytes = new byte[SourceSize]; var source = new MemoryStream(sourceBytes); var destination = new MemoryStream(); var clock = new ManualClock(); var sourceWaitTime = TimeSpan.FromMilliseconds(789); // Every second read triggers ContentTransferring var destinationWaitTime = TimeSpan.FromMilliseconds(42); const int BytesPerRead = 3; var contentReads = (int)Math.Ceiling((double)SourceSize / BytesPerRead); using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); await StreamCopier.CopyAsync( isRequest, new SlowStream(source, clock, sourceWaitTime) { MaxBytesPerRead = BytesPerRead }, new SlowStream(destination, clock, destinationWaitTime), SourceSize, clock, cts, cts.Token); Assert.Equal(sourceBytes, destination.ToArray()); AssertContentTransferred(events, isRequest, SourceSize, iops: contentReads + 1, firstReadTime: sourceWaitTime, readTime: (contentReads + 1) * sourceWaitTime, writeTime: contentReads * destinationWaitTime); var transferringEvents = events.Where(e => e.EventName == "ContentTransferring").ToArray(); Assert.Equal(contentReads / 2, transferringEvents.Length); for (var i = 0; i < transferringEvents.Length; i++) { var payload = transferringEvents[i].Payload; Assert.Equal(5, payload.Count); Assert.Equal(isRequest, (bool)payload[0]); var contentLength = (long)payload[1]; var iops = (long)payload[2]; Assert.Equal((i + 1) * 2, iops); if (contentLength % BytesPerRead == 0) { Assert.Equal(iops * BytesPerRead, contentLength); } else { Assert.Equal(transferringEvents.Length - 1, i); Assert.Equal(SourceSize, contentLength); } var readTime = new TimeSpan((long)payload[3]); Assert.Equal(iops * sourceWaitTime, readTime, new ApproximateTimeSpanComparer()); var writeTime = new TimeSpan((long)payload[4]); Assert.Equal(iops * destinationWaitTime, writeTime, new ApproximateTimeSpanComparer()); } }