diff --git a/src/Microsoft.Net.Http.Server/LogHelper.cs b/src/Microsoft.Net.Http.Server/LogHelper.cs index 6ec1c67a7b..e96ba71b10 100644 --- a/src/Microsoft.Net.Http.Server/LogHelper.cs +++ b/src/Microsoft.Net.Http.Server/LogHelper.cs @@ -53,6 +53,18 @@ namespace Microsoft.Net.Http.Server } } + internal static void LogDebug(ILogger logger, string location, Exception exception) + { + if (logger == null) + { + Debug.WriteLine(exception); + } + else + { + logger.LogDebug(0, exception, location); + } + } + internal static void LogException(ILogger logger, string location, Exception exception) { if (logger == null) @@ -61,7 +73,7 @@ namespace Microsoft.Net.Http.Server } else { - logger.LogError(location, exception); + logger.LogError(0, exception, location); } } diff --git a/src/Microsoft.Net.Http.Server/NativeInterop/DisconnectListener.cs b/src/Microsoft.Net.Http.Server/NativeInterop/DisconnectListener.cs index ff45668da9..e96ed834a6 100644 --- a/src/Microsoft.Net.Http.Server/NativeInterop/DisconnectListener.cs +++ b/src/Microsoft.Net.Http.Server/NativeInterop/DisconnectListener.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Concurrent; using System.ComponentModel; +using System.Diagnostics; using System.Threading; using Microsoft.Extensions.Logging; @@ -75,6 +76,7 @@ namespace Microsoft.Net.Http.Server // Create a nativeOverlapped callback so we can register for disconnect callback var cts = new CancellationTokenSource(); + var returnToken = cts.Token; SafeNativeOverlapped nativeOverlapped = null; var boundHandle = _requestQueue.BoundHandle; @@ -97,8 +99,6 @@ namespace Microsoft.Net.Http.Server { LogHelper.LogException(_logger, "CreateDisconnectToken Callback", exception); } - - cts.Dispose(); }, null, null)); @@ -117,19 +117,24 @@ namespace Microsoft.Net.Http.Server if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) { - // We got an unknown result so return a None - // TODO: return a canceled token? - return CancellationToken.None; + // We got an unknown result, assume the connection has been closed. + nativeOverlapped.Dispose(); + ConnectionCancellation ignored; + _connectionCancellationTokens.TryRemove(connectionId, out ignored); + LogHelper.LogDebug(_logger, "HttpWaitForDisconnectEx", new Win32Exception((int)statusCode)); + cts.Cancel(); } if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess) { - // IO operation completed synchronously - callback won't be called to signal completion. - // TODO: return a canceled token? - return CancellationToken.None; + // IO operation completed synchronously - callback won't be called to signal completion + nativeOverlapped.Dispose(); + ConnectionCancellation ignored; + _connectionCancellationTokens.TryRemove(connectionId, out ignored); + cts.Cancel(); } - return cts.Token; + return returnToken; } private class ConnectionCancellation diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStream.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStream.cs index be0d5ad1a8..cacb354851 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStream.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStream.cs @@ -38,7 +38,8 @@ namespace Microsoft.Net.Http.Server { private RequestContext _requestContext; private long _leftToWrite = long.MinValue; - private bool _closed; + private bool _skipWrites; + private bool _disposed; private bool _inOpaqueMode; // The last write needs special handling to cancel. @@ -60,6 +61,8 @@ namespace Microsoft.Net.Http.Server private ILogger Logger => RequestContext.Server.Logger; + internal bool ThrowWriteExceptions => RequestContext.Server.Settings.ThrowWriteExceptions; + public override bool CanSeek { get @@ -107,7 +110,7 @@ namespace Microsoft.Net.Http.Server // Send headers public override void Flush() { - if (_closed) + if (_disposed) { return; } @@ -119,6 +122,11 @@ namespace Microsoft.Net.Http.Server { Debug.Assert(!(endOfRequest && data.Count > 0), "Data is not supported at the end of the request."); + if (_skipWrites) + { + return; + } + var started = _requestContext.Response.HasStarted; if (data.Count == 0 && started && !endOfRequest) { @@ -170,11 +178,6 @@ namespace Microsoft.Net.Http.Server SafeNativeOverlapped.Zero, IntPtr.Zero); } - - if (_requestContext.Server.Settings.IgnoreWriteExceptions) - { - statusCode = UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS; - } } } finally @@ -186,10 +189,19 @@ namespace Microsoft.Net.Http.Server // Don't throw for disconnects, we were already finished with the response. && (!endOfRequest || (statusCode != ErrorCodes.ERROR_CONNECTION_INVALID && statusCode != ErrorCodes.ERROR_INVALID_PARAMETER))) { - Exception exception = new IOException(string.Empty, new WebListenerException((int)statusCode)); - LogHelper.LogException(Logger, "Flush", exception); - Abort(); - throw exception; + if (ThrowWriteExceptions) + { + var exception = new IOException(string.Empty, new WebListenerException((int)statusCode)); + LogHelper.LogException(Logger, "Flush", exception); + Abort(); + throw exception; + } + else + { + // Abort the request but do not close the stream, let future writes complete silently + LogHelper.LogDebug(Logger, "Flush", $"Ignored write exception: {statusCode}"); + Abort(dispose: false); + } } } @@ -271,7 +283,7 @@ namespace Microsoft.Net.Http.Server public override Task FlushAsync(CancellationToken cancellationToken) { - if (_closed) + if (_disposed) { return Helpers.CompletedTask(); } @@ -281,6 +293,11 @@ namespace Microsoft.Net.Http.Server // Simpler than Flush because it will never be called at the end of the request from Dispose. private unsafe Task FlushInternalAsync(ArraySegment data, CancellationToken cancellationToken) { + if (_skipWrites) + { + return Helpers.CompletedTask(); + } + var started = _requestContext.Response.HasStarted; if (data.Count == 0 && started) { @@ -288,10 +305,10 @@ namespace Microsoft.Net.Http.Server return Helpers.CompletedTask(); } - var cancellationRegistration = default(CancellationTokenRegistration); - if (cancellationToken.CanBeCanceled) + if (cancellationToken.IsCancellationRequested) { - cancellationRegistration = RequestContext.RegisterForCancellation(cancellationToken); + Abort(ThrowWriteExceptions); + return Helpers.CanceledTask(); } var flags = ComputeLeftToWrite(); @@ -303,7 +320,7 @@ namespace Microsoft.Net.Http.Server UpdateWritenCount((uint)data.Count); uint statusCode = 0; var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked; - var asyncResult = new ResponseStreamAsyncResult(this, data, chunked, cancellationRegistration); + var asyncResult = new ResponseStreamAsyncResult(this, data, chunked, cancellationToken); uint bytesSent = 0; try { @@ -337,18 +354,25 @@ namespace Microsoft.Net.Http.Server if (statusCode != ErrorCodes.ERROR_SUCCESS && statusCode != ErrorCodes.ERROR_IO_PENDING) { - asyncResult.Dispose(); - if (_requestContext.Server.Settings.IgnoreWriteExceptions && started) + if (cancellationToken.IsCancellationRequested) { - asyncResult.Complete(); + LogHelper.LogDebug(Logger, "FlushAsync", $"Write cancelled with error code: {statusCode}"); + asyncResult.Cancel(ThrowWriteExceptions); } - else + else if (ThrowWriteExceptions) { + asyncResult.Dispose(); Exception exception = new IOException(string.Empty, new WebListenerException((int)statusCode)); LogHelper.LogException(Logger, "FlushAsync", exception); Abort(); throw exception; } + else + { + // Abort the request but do not close the stream, let future writes complete silently + LogHelper.LogDebug(Logger, "FlushAsync", $"Ignored write exception: {statusCode}"); + asyncResult.FailSilently(); + } } if (statusCode == ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess) @@ -397,9 +421,16 @@ namespace Microsoft.Net.Http.Server #endregion - internal void Abort() + internal void Abort(bool dispose = true) { - _closed = true; + if (dispose) + { + _disposed = true; + } + else + { + _skipWrites = true; + } _requestContext.Abort(); } @@ -476,14 +507,10 @@ namespace Microsoft.Net.Http.Server ((Task)asyncResult).GetAwaiter().GetResult(); } - public override unsafe Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { // Validates for null and bounds. Allows count == 0. var data = new ArraySegment(buffer, offset, count); - if (cancellationToken.IsCancellationRequested) - { - return Helpers.CanceledTask(); - } CheckDisposed(); // TODO: Verbose log parameters @@ -523,33 +550,34 @@ namespace Microsoft.Net.Http.Server internal unsafe Task SendFileAsyncCore(string fileName, long offset, long? count, CancellationToken cancellationToken) { + if (_skipWrites) + { + return Helpers.CompletedTask(); + } + var flags = ComputeLeftToWrite(); if (count == 0 && _leftToWrite != 0) { return Helpers.CompletedTask(); } + if (_leftToWrite >= 0 && count > _leftToWrite) { throw new InvalidOperationException(Resources.Exception_TooMuchWritten); } - // TODO: Verbose log if (cancellationToken.IsCancellationRequested) { + Abort(ThrowWriteExceptions); return Helpers.CanceledTask(); } - - var cancellationRegistration = default(CancellationTokenRegistration); - if (cancellationToken.CanBeCanceled) - { - cancellationRegistration = RequestContext.RegisterForCancellation(cancellationToken); - } + // TODO: Verbose log uint statusCode; uint bytesSent = 0; var started = _requestContext.Response.HasStarted; var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked; - ResponseStreamAsyncResult asyncResult = new ResponseStreamAsyncResult(this, fileName, offset, count, chunked, cancellationRegistration); + var asyncResult = new ResponseStreamAsyncResult(this, fileName, offset, count, chunked, cancellationToken); long bytesWritten; if (chunked) @@ -601,18 +629,25 @@ namespace Microsoft.Net.Http.Server if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING) { - asyncResult.Dispose(); - if (_requestContext.Server.Settings.IgnoreWriteExceptions && started) + if (cancellationToken.IsCancellationRequested) { - asyncResult.Complete(); + LogHelper.LogDebug(Logger, "SendFileAsync", $"Write cancelled with error code: {statusCode}"); + asyncResult.Cancel(ThrowWriteExceptions); } - else + else if (ThrowWriteExceptions) { - Exception exception = new IOException(string.Empty, new WebListenerException((int)statusCode)); + asyncResult.Dispose(); + var exception = new IOException(string.Empty, new WebListenerException((int)statusCode)); LogHelper.LogException(Logger, "SendFileAsync", exception); Abort(); throw exception; } + else + { + // Abort the request but do not close the stream, let future writes complete silently + LogHelper.LogDebug(Logger, "SendFileAsync", $"Ignored write exception: {statusCode}"); + asyncResult.FailSilently(); + } } if (statusCode == ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess) @@ -642,7 +677,7 @@ namespace Microsoft.Net.Http.Server if (_leftToWrite == 0) { // in this case we already passed 0 as the flag, so we don't need to call HttpSendResponseEntityBody() when we Close() - _closed = true; + _disposed = true; } } } @@ -653,11 +688,11 @@ namespace Microsoft.Net.Http.Server { if (disposing) { - if (_closed) + if (_disposed) { return; } - _closed = true; + _disposed = true; FlushInternal(endOfRequest: true); } } @@ -688,7 +723,7 @@ namespace Microsoft.Net.Http.Server private void CheckDisposed() { - if (_closed) + if (_disposed) { throw new ObjectDisposedException(GetType().FullName); } diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStreamAsyncResult.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStreamAsyncResult.cs index 73fa62ff28..49f6ab154b 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStreamAsyncResult.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStreamAsyncResult.cs @@ -41,18 +41,26 @@ namespace Microsoft.Net.Http.Server private ResponseStream _responseStream; private TaskCompletionSource _tcs; private uint _bytesSent; + private CancellationToken _cancellationToken; private CancellationTokenRegistration _cancellationRegistration; - internal ResponseStreamAsyncResult(ResponseStream responseStream, CancellationTokenRegistration cancellationRegistration) + internal ResponseStreamAsyncResult(ResponseStream responseStream, CancellationToken cancellationToken) { _responseStream = responseStream; _tcs = new TaskCompletionSource(); + + var cancellationRegistration = default(CancellationTokenRegistration); + if (cancellationToken.CanBeCanceled) + { + cancellationRegistration = _responseStream.RequestContext.RegisterForCancellation(cancellationToken); + } + _cancellationToken = cancellationToken; _cancellationRegistration = cancellationRegistration; } internal ResponseStreamAsyncResult(ResponseStream responseStream, ArraySegment data, bool chunked, - CancellationTokenRegistration cancellationRegistration) - : this(responseStream, cancellationRegistration) + CancellationToken cancellationToken) + : this(responseStream, cancellationToken) { var boundHandle = _responseStream.RequestContext.Server.RequestQueue.BoundHandle; object[] objectsToPin; @@ -107,8 +115,8 @@ namespace Microsoft.Net.Http.Server } internal ResponseStreamAsyncResult(ResponseStream responseStream, string fileName, long offset, - long? count, bool chunked, CancellationTokenRegistration cancellationRegistration) - : this(responseStream, cancellationRegistration) + long? count, bool chunked, CancellationToken cancellationToken) + : this(responseStream, cancellationToken) { var boundHandle = responseStream.RequestContext.Server.RequestQueue.BoundHandle; @@ -260,11 +268,27 @@ namespace Microsoft.Net.Http.Server [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Redirecting to callback")] private static void IOCompleted(ResponseStreamAsyncResult asyncResult, uint errorCode, uint numBytes) { + var logger = asyncResult._responseStream.RequestContext.Logger; try { if (errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF) { - asyncResult.Fail(new IOException(string.Empty, new WebListenerException((int)errorCode))); + if (asyncResult._cancellationToken.IsCancellationRequested) + { + LogHelper.LogDebug(logger, "FlushAsync.IOCompleted", $"Write cancelled with error code: {errorCode}"); + asyncResult.Cancel(asyncResult._responseStream.ThrowWriteExceptions); + } + else if (asyncResult._responseStream.ThrowWriteExceptions) + { + var exception = new IOException(string.Empty, new WebListenerException((int)errorCode)); + LogHelper.LogException(logger, "FlushAsync.IOCompleted", exception); + asyncResult.Fail(exception); + } + else + { + LogHelper.LogDebug(logger, "FlushAsync.IOCompleted", $"Ignored write exception: {errorCode}"); + asyncResult.FailSilently(); + } } else { @@ -285,6 +309,7 @@ namespace Microsoft.Net.Http.Server } catch (Exception e) { + LogHelper.LogException(logger, "FlushAsync.IOCompleted", e); asyncResult.Fail(e); } } @@ -297,15 +322,30 @@ namespace Microsoft.Net.Http.Server internal void Complete() { - _tcs.TrySetResult(null); Dispose(); + _tcs.TrySetResult(null); + } + + internal void FailSilently() + { + Dispose(); + // Abort the request but do not close the stream, let future writes complete silently + _responseStream.Abort(dispose: false); + _tcs.TrySetResult(null); + } + + internal void Cancel(bool dispose) + { + Dispose(); + _responseStream.Abort(dispose); + _tcs.TrySetCanceled(); } internal void Fail(Exception ex) { - _tcs.TrySetException(ex); Dispose(); _responseStream.Abort(); + _tcs.TrySetException(ex); } public object AsyncState diff --git a/src/Microsoft.Net.Http.Server/WebListenerSettings.cs b/src/Microsoft.Net.Http.Server/WebListenerSettings.cs index dd65bc5c3f..c1f8f3c39d 100644 --- a/src/Microsoft.Net.Http.Server/WebListenerSettings.cs +++ b/src/Microsoft.Net.Http.Server/WebListenerSettings.cs @@ -69,13 +69,11 @@ namespace Microsoft.Net.Http.Server /// public TimeoutManager Timeouts { get; } = new TimeoutManager(); - - // TODO: https://github.com/aspnet/WebListener/issues/173 /// /// Gets or Sets if response body writes that fail due to client disconnects should throw exceptions or - /// complete normally. The default is true. + /// complete normally. The default is false. /// - internal bool IgnoreWriteExceptions { get; set; } = true; + public bool ThrowWriteExceptions { get; set; } /// /// Gets or sets the maximum number of requests that will be queued up in Http.Sys. diff --git a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseBodyTests.cs b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseBodyTests.cs index 3a2b14fefe..27ad36cabf 100644 --- a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseBodyTests.cs +++ b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseBodyTests.cs @@ -8,7 +8,6 @@ using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Testing.xunit; using Xunit; namespace Microsoft.Net.Http.Server @@ -21,14 +20,14 @@ namespace Microsoft.Net.Http.Server string address; using (var server = Utilities.CreateHttpServer(out address)) { - Task responseTask = SendRequestAsync(address); + var responseTask = SendRequestAsync(address); var context = await server.AcceptAsync(); context.Response.Body.Write(new byte[10], 0, 10); await context.Response.Body.WriteAsync(new byte[10], 0, 10); context.Dispose(); - HttpResponseMessage response = await responseTask; + var response = await responseTask; Assert.Equal(200, (int)response.StatusCode); Assert.Equal(new Version(1, 1), response.Version); IEnumerable ignored; @@ -44,7 +43,7 @@ namespace Microsoft.Net.Http.Server string address; using (var server = Utilities.CreateHttpServer(out address)) { - Task responseTask = SendRequestAsync(address); + var responseTask = SendRequestAsync(address); var context = await server.AcceptAsync(); context.Response.Body.Write(new byte[10], 0, 10); @@ -52,7 +51,7 @@ namespace Microsoft.Net.Http.Server await context.Response.Body.WriteAsync(new byte[10], 0, 10); context.Dispose(); - HttpResponseMessage response = await responseTask; + var response = await responseTask; Assert.Equal(200, (int)response.StatusCode); IEnumerable contentLength; Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); @@ -67,7 +66,7 @@ namespace Microsoft.Net.Http.Server string address; using (var server = Utilities.CreateHttpServer(out address)) { - Task responseTask = SendRequestAsync(address); + var responseTask = SendRequestAsync(address); var context = await server.AcceptAsync(); context.Response.Headers["transfeR-Encoding"] = "CHunked"; @@ -76,7 +75,7 @@ namespace Microsoft.Net.Http.Server await stream.WriteAsync(responseBytes, 0, responseBytes.Length); context.Dispose(); - HttpResponseMessage response = await responseTask; + var response = await responseTask; Assert.Equal(200, (int)response.StatusCode); Assert.Equal(new Version(1, 1), response.Version); IEnumerable ignored; @@ -92,11 +91,11 @@ namespace Microsoft.Net.Http.Server string address; using (var server = Utilities.CreateHttpServer(out address)) { - Task responseTask = SendRequestAsync(address); + var responseTask = SendRequestAsync(address); var context = await server.AcceptAsync(); context.Response.Headers["Content-lenGth"] = " 30 "; - Stream stream = context.Response.Body; + var stream = context.Response.Body; #if NET451 stream.EndWrite(stream.BeginWrite(new byte[10], 0, 10, null, null)); #else @@ -106,7 +105,7 @@ namespace Microsoft.Net.Http.Server await stream.WriteAsync(new byte[10], 0, 10); context.Dispose(); - HttpResponseMessage response = await responseTask; + var response = await responseTask; Assert.Equal(200, (int)response.StatusCode); Assert.Equal(new Version(1, 1), response.Version); IEnumerable contentLength; @@ -123,7 +122,7 @@ namespace Microsoft.Net.Http.Server string address; using (var server = Utilities.CreateHttpServer(out address)) { - Task responseTask = SendRequestAsync(address); + var responseTask = SendRequestAsync(address); var context = await server.AcceptAsync(); context.Response.Headers["Content-lenGth"] = " 20 "; @@ -144,7 +143,7 @@ namespace Microsoft.Net.Http.Server string address; using (var server = Utilities.CreateHttpServer(out address)) { - Task responseTask = SendRequestAsync(address); + var responseTask = SendRequestAsync(address); var context = await server.AcceptAsync(); context.Response.Headers["Content-lenGth"] = " 20 "; @@ -161,7 +160,7 @@ namespace Microsoft.Net.Http.Server string address; using (var server = Utilities.CreateHttpServer(out address)) { - Task responseTask = SendRequestAsync(address); + var responseTask = SendRequestAsync(address); var context = await server.AcceptAsync(); context.Response.Headers["Content-lenGth"] = " 10 "; @@ -179,7 +178,7 @@ namespace Microsoft.Net.Http.Server string address; using (var server = Utilities.CreateHttpServer(out address)) { - Task responseTask = SendRequestAsync(address); + var responseTask = SendRequestAsync(address); var context = await server.AcceptAsync(); context.Response.Headers["Content-lenGth"] = " 10 "; @@ -204,7 +203,7 @@ namespace Microsoft.Net.Http.Server string address; using (var server = Utilities.CreateHttpServer(out address)) { - Task responseTask = SendRequestAsync(address); + var responseTask = SendRequestAsync(address); var context = await server.AcceptAsync(); context.Response.Body.Write(new byte[10], 0, 0); @@ -212,7 +211,7 @@ namespace Microsoft.Net.Http.Server await context.Response.Body.WriteAsync(new byte[10], 0, 0); context.Dispose(); - HttpResponseMessage response = await responseTask; + var response = await responseTask; Assert.Equal(200, (int)response.StatusCode); Assert.Equal(new Version(1, 1), response.Version); IEnumerable ignored; @@ -228,7 +227,7 @@ namespace Microsoft.Net.Http.Server string address; using (var server = Utilities.CreateHttpServer(out address)) { - Task responseTask = SendRequestAsync(address); + var responseTask = SendRequestAsync(address); var context = await server.AcceptAsync(); var cts = new CancellationTokenSource(); @@ -237,7 +236,7 @@ namespace Microsoft.Net.Http.Server await context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token); context.Dispose(); - HttpResponseMessage response = await responseTask; + var response = await responseTask; Assert.Equal(200, (int)response.StatusCode); Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync()); } @@ -249,29 +248,30 @@ namespace Microsoft.Net.Http.Server string address; using (var server = Utilities.CreateHttpServer(out address)) { - Task responseTask = SendRequestAsync(address); + var responseTask = SendRequestAsync(address); var context = await server.AcceptAsync(); var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromSeconds(1)); + cts.CancelAfter(TimeSpan.FromSeconds(10)); // First write sends headers await context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token); await context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token); context.Dispose(); - HttpResponseMessage response = await responseTask; + var response = await responseTask; Assert.Equal(200, (int)response.StatusCode); Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync()); } } [Fact] - public async Task ResponseBody_FirstWriteAsyncWithCanceledCancellationToken_CancelsButDoesNotAbort() + public async Task ResponseBodyWriteExceptions_FirstWriteAsyncWithCanceledCancellationToken_CancelsAndAborts() { string address; using (var server = Utilities.CreateHttpServer(out address)) { - Task responseTask = SendRequestAsync(address); + server.Settings.ThrowWriteExceptions = true; + var responseTask = SendRequestAsync(address); var context = await server.AcceptAsync(); var cts = new CancellationTokenSource(); @@ -280,20 +280,57 @@ namespace Microsoft.Net.Http.Server var writeTask = context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token); Assert.True(writeTask.IsCanceled); context.Dispose(); - - HttpResponseMessage response = await responseTask; - Assert.Equal(200, (int)response.StatusCode); - Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync()); +#if NET451 + // .NET 4.5 HttpClient automatically retries a request if it does not get a response. + context = await server.AcceptAsync(); + cts = new CancellationTokenSource(); + cts.Cancel(); + // First write sends headers + writeTask = context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token); + Assert.True(writeTask.IsCanceled); + context.Dispose(); +#endif + await Assert.ThrowsAsync(() => responseTask); } } [Fact] - public async Task ResponseBody_SecondWriteAsyncWithCanceledCancellationToken_CancelsButDoesNotAbort() + public async Task ResponseBody_FirstWriteAsyncWithCanceledCancellationToken_CancelsAndAborts() { string address; using (var server = Utilities.CreateHttpServer(out address)) { - Task responseTask = SendRequestAsync(address); + var responseTask = SendRequestAsync(address); + + var context = await server.AcceptAsync(); + var cts = new CancellationTokenSource(); + cts.Cancel(); + // First write sends headers + var writeTask = context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token); + Assert.True(writeTask.IsCanceled); + context.Dispose(); +#if NET451 + // .NET 4.5 HttpClient automatically retries a request if it does not get a response. + context = await server.AcceptAsync(); + cts = new CancellationTokenSource(); + cts.Cancel(); + // First write sends headers + writeTask = context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token); + Assert.True(writeTask.IsCanceled); + context.Dispose(); +#endif + await Assert.ThrowsAsync(() => responseTask); + } + } + + [Fact] + public async Task ResponseBodyWriteExceptions_SecondWriteAsyncWithCanceledCancellationToken_CancelsAndAborts() + { + string address; + using (var server = Utilities.CreateHttpServer(out address)) + { + server.Settings.ThrowWriteExceptions = true; + var responseTask = SendRequestAsync(address); var context = await server.AcceptAsync(); var cts = new CancellationTokenSource(); @@ -304,17 +341,272 @@ namespace Microsoft.Net.Http.Server Assert.True(writeTask.IsCanceled); context.Dispose(); - HttpResponseMessage response = await responseTask; - Assert.Equal(200, (int)response.StatusCode); - Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync()); + await Assert.ThrowsAsync(() => responseTask); } } - private async Task SendRequestAsync(string uri) + [Fact] + public async Task ResponseBody_SecondWriteAsyncWithCanceledCancellationToken_CancelsAndAborts() + { + string address; + using (var server = Utilities.CreateHttpServer(out address)) + { + var responseTask = SendRequestAsync(address); + + var context = await server.AcceptAsync(); + var cts = new CancellationTokenSource(); + // First write sends headers + await context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token); + cts.Cancel(); + var writeTask = context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token); + Assert.True(writeTask.IsCanceled); + context.Dispose(); + + await Assert.ThrowsAsync(() => responseTask); + } + } + + [Fact] + public async Task ResponseBodyWriteExceptions_ClientDisconnectsBeforeFirstWrite_WriteThrows() + { + string address; + using (var server = Utilities.CreateHttpServer(out address)) + { + server.Settings.ThrowWriteExceptions = true; + var cts = new CancellationTokenSource(); + var responseTask = SendRequestAsync(address, cts.Token); + + var context = await server.AcceptAsync(); + // First write sends headers + cts.Cancel(); + await Assert.ThrowsAsync(() => responseTask); + Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5))); + Assert.Throws(() => + { + // It can take several tries before Write notices the disconnect. + for (int i = 0; i < 1000; i++) + { + context.Response.Body.Write(new byte[1000], 0, 1000); + } + }); + + Assert.Throws(() => context.Response.Body.Write(new byte[1000], 0, 1000)); + + context.Dispose(); + } + } + + [Fact] + public async Task ResponseBodyWriteExceptions_ClientDisconnectsBeforeFirstWriteAsync_WriteThrows() + { + string address; + using (var server = Utilities.CreateHttpServer(out address)) + { + server.Settings.ThrowWriteExceptions = true; + var cts = new CancellationTokenSource(); + var responseTask = SendRequestAsync(address, cts.Token); + + var context = await server.AcceptAsync(); + + // First write sends headers + cts.Cancel(); + await Assert.ThrowsAsync(() => responseTask); + + Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5))); + await Assert.ThrowsAsync(async () => + { + // It can take several tries before Write notices the disconnect. + for (int i = 0; i < 1000; i++) + { + await context.Response.Body.WriteAsync(new byte[1000], 0, 1000); + } + }); + + await Assert.ThrowsAsync(() => context.Response.Body.WriteAsync(new byte[1000], 0, 1000)); + + context.Dispose(); + } + } + + [Fact] + public async Task ResponseBody_ClientDisconnectsBeforeFirstWrite_WriteCompletesSilently() + { + string address; + using (var server = Utilities.CreateHttpServer(out address)) + { + var cts = new CancellationTokenSource(); + var responseTask = SendRequestAsync(address, cts.Token); + + var context = await server.AcceptAsync(); + // First write sends headers + cts.Cancel(); + await Assert.ThrowsAsync(() => responseTask); + Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5))); + // It can take several tries before Write notices the disconnect. + for (int i = 0; i < 100; i++) + { + context.Response.Body.Write(new byte[1000], 0, 1000); + } + context.Dispose(); + } + } + + [Fact] + public async Task ResponseBody_ClientDisconnectsBeforeFirstWriteAsync_WriteCompletesSilently() + { + string address; + using (var server = Utilities.CreateHttpServer(out address)) + { + var cts = new CancellationTokenSource(); + var responseTask = SendRequestAsync(address, cts.Token); + + var context = await server.AcceptAsync(); + // First write sends headers + cts.Cancel(); + await Assert.ThrowsAsync(() => responseTask); + Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5))); + // It can take several tries before Write notices the disconnect. + for (int i = 0; i < 100; i++) + { + await context.Response.Body.WriteAsync(new byte[1000], 0, 1000); + } + context.Dispose(); + } + } + + [Fact] + public async Task ResponseBodyWriteExceptions_ClientDisconnectsBeforeSecondWrite_WriteThrows() + { + string address; + using (var server = Utilities.CreateHttpServer(out address)) + { + server.Settings.ThrowWriteExceptions = true; + RequestContext context; + using (var client = new HttpClient()) + { + var responseTask = client.GetAsync(address, HttpCompletionOption.ResponseHeadersRead); + + context = await server.AcceptAsync(); + // First write sends headers + context.Response.Body.Write(new byte[10], 0, 10); + + var response = await responseTask; + response.EnsureSuccessStatusCode(); + response.Dispose(); + } + + Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5))); + Assert.Throws(() => + { + // It can take several tries before Write notices the disconnect. + for (int i = 0; i < 100; i++) + { + context.Response.Body.Write(new byte[1000], 0, 1000); + } + }); + context.Dispose(); + } + } + + [Fact] + public async Task ResponseBodyWriteExceptions_ClientDisconnectsBeforeSecondWriteAsync_WriteThrows() + { + string address; + using (var server = Utilities.CreateHttpServer(out address)) + { + server.Settings.ThrowWriteExceptions = true; + RequestContext context; + using (var client = new HttpClient()) + { + var responseTask = client.GetAsync(address, HttpCompletionOption.ResponseHeadersRead); + + context = await server.AcceptAsync(); + // First write sends headers + await context.Response.Body.WriteAsync(new byte[10], 0, 10); + + var response = await responseTask; + response.EnsureSuccessStatusCode(); + response.Dispose(); + } + + Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5))); + await Assert.ThrowsAsync(async () => + { + // It can take several tries before Write notices the disconnect. + for (int i = 0; i < 100; i++) + { + await context.Response.Body.WriteAsync(new byte[1000], 0, 1000); + } + }); + context.Dispose(); + } + } + + [Fact] + public async Task ResponseBody_ClientDisconnectsBeforeSecondWrite_WriteCompletesSilently() + { + string address; + using (var server = Utilities.CreateHttpServer(out address)) + { + RequestContext context; + using (var client = new HttpClient()) + { + var responseTask = client.GetAsync(address, HttpCompletionOption.ResponseHeadersRead); + + context = await server.AcceptAsync(); + // First write sends headers + context.Response.Body.Write(new byte[10], 0, 10); + + var response = await responseTask; + response.EnsureSuccessStatusCode(); + response.Dispose(); + } + + Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5))); + // It can take several tries before Write notices the disconnect. + for (int i = 0; i < 10; i++) + { + context.Response.Body.Write(new byte[1000], 0, 1000); + } + context.Dispose(); + } + } + + [Fact] + public async Task ResponseBody_ClientDisconnectsBeforeSecondWriteAsync_WriteCompletesSilently() + { + string address; + using (var server = Utilities.CreateHttpServer(out address)) + { + RequestContext context; + using (var client = new HttpClient()) + { + var responseTask = client.GetAsync(address, HttpCompletionOption.ResponseHeadersRead); + + context = await server.AcceptAsync(); + // First write sends headers + await context.Response.Body.WriteAsync(new byte[10], 0, 10); + + var response = await responseTask; + response.EnsureSuccessStatusCode(); + response.Dispose(); + } + + Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5))); + // It can take several tries before Write notices the disconnect. + for (int i = 0; i < 10; i++) + { + await context.Response.Body.WriteAsync(new byte[1000], 0, 1000); + } + context.Dispose(); + } + } + + private async Task SendRequestAsync(string uri, CancellationToken cancellationToken = new CancellationToken()) { using (HttpClient client = new HttpClient()) { - return await client.GetAsync(uri); + return await client.GetAsync(uri, cancellationToken); } } } diff --git a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseSendFileTests.cs b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseSendFileTests.cs index b29aa197a4..89073fefbc 100644 --- a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseSendFileTests.cs +++ b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseSendFileTests.cs @@ -298,11 +298,276 @@ namespace Microsoft.Net.Http.Server } } - private async Task SendRequestAsync(string uri) + [Fact] + public async Task ResponseSendFile_WithActiveCancellationToken_Success() + { + string address; + using (var server = Utilities.CreateHttpServer(out address)) + { + var responseTask = SendRequestAsync(address); + + var context = await server.AcceptAsync(); + var cts = new CancellationTokenSource(); + // First write sends headers + await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token); + await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + Assert.Equal(200, (int)response.StatusCode); + Assert.Equal(FileLength * 2, (await response.Content.ReadAsByteArrayAsync()).Length); + } + } + + [Fact] + public async Task ResponseSendFile_WithTimerCancellationToken_Success() + { + string address; + using (var server = Utilities.CreateHttpServer(out address)) + { + var responseTask = SendRequestAsync(address); + + var context = await server.AcceptAsync(); + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(10)); + // First write sends headers + await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token); + await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token); + context.Dispose(); + + HttpResponseMessage response = await responseTask; + Assert.Equal(200, (int)response.StatusCode); + Assert.Equal(FileLength * 2, (await response.Content.ReadAsByteArrayAsync()).Length); + } + } + + [Fact] + public async Task ResponseSendFileWriteExceptions_FirstCallWithCanceledCancellationToken_CancelsAndAborts() + { + string address; + using (var server = Utilities.CreateHttpServer(out address)) + { + server.Settings.ThrowWriteExceptions = true; + var responseTask = SendRequestAsync(address); + + var context = await server.AcceptAsync(); + var cts = new CancellationTokenSource(); + cts.Cancel(); + // First write sends headers + var writeTask = context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token); + Assert.True(writeTask.IsCanceled); + context.Dispose(); +#if NET451 + // .NET 4.5 HttpClient automatically retries a request if it does not get a response. + context = await server.AcceptAsync(); + cts = new CancellationTokenSource(); + cts.Cancel(); + // First write sends headers + writeTask = context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token); + Assert.True(writeTask.IsCanceled); + context.Dispose(); +#endif + await Assert.ThrowsAsync(() => responseTask); + } + } + + [Fact] + public async Task ResponseSendFile_FirstSendWithCanceledCancellationToken_CancelsAndAborts() + { + string address; + using (var server = Utilities.CreateHttpServer(out address)) + { + var responseTask = SendRequestAsync(address); + + var context = await server.AcceptAsync(); + var cts = new CancellationTokenSource(); + cts.Cancel(); + // First write sends headers + var writeTask = context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token); + Assert.True(writeTask.IsCanceled); + context.Dispose(); +#if NET451 + // .NET 4.5 HttpClient automatically retries a request if it does not get a response. + context = await server.AcceptAsync(); + cts = new CancellationTokenSource(); + cts.Cancel(); + // First write sends headers + writeTask = context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token); + Assert.True(writeTask.IsCanceled); + context.Dispose(); +#endif + await Assert.ThrowsAsync(() => responseTask); + } + } + + [Fact] + public async Task ResponseSendFileExceptions_SecondSendWithCanceledCancellationToken_CancelsAndAborts() + { + string address; + using (var server = Utilities.CreateHttpServer(out address)) + { + server.Settings.ThrowWriteExceptions = true; + var responseTask = SendRequestAsync(address); + + var context = await server.AcceptAsync(); + var cts = new CancellationTokenSource(); + // First write sends headers + await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token); + cts.Cancel(); + var writeTask = context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token); + Assert.True(writeTask.IsCanceled); + context.Dispose(); + + await Assert.ThrowsAsync(() => responseTask); + } + } + + [Fact] + public async Task ResponseSendFile_SecondSendWithCanceledCancellationToken_CancelsAndAborts() + { + string address; + using (var server = Utilities.CreateHttpServer(out address)) + { + var responseTask = SendRequestAsync(address); + + var context = await server.AcceptAsync(); + var cts = new CancellationTokenSource(); + // First write sends headers + await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token); + cts.Cancel(); + var writeTask = context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token); + Assert.True(writeTask.IsCanceled); + context.Dispose(); + + await Assert.ThrowsAsync(() => responseTask); + } + } + + [Fact] + public async Task ResponseSendFileExceptions_ClientDisconnectsBeforeFirstSend_SendThrows() + { + string address; + using (var server = Utilities.CreateHttpServer(out address)) + { + server.Settings.ThrowWriteExceptions = true; + var cts = new CancellationTokenSource(); + var responseTask = SendRequestAsync(address, cts.Token); + + var context = await server.AcceptAsync(); + + // First write sends headers + cts.Cancel(); + await Assert.ThrowsAsync(() => responseTask); + + Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5))); + await Assert.ThrowsAsync(async () => + { + // It can take several tries before Send notices the disconnect. + for (int i = 0; i < 1000; i++) + { + await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); + } + }); + + await Assert.ThrowsAsync(() => + context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None)); + + context.Dispose(); + } + } + + [Fact] + public async Task ResponseSendFile_ClientDisconnectsBeforeFirstSend_SendCompletesSilently() + { + string address; + using (var server = Utilities.CreateHttpServer(out address)) + { + var cts = new CancellationTokenSource(); + var responseTask = SendRequestAsync(address, cts.Token); + + var context = await server.AcceptAsync(); + // First write sends headers + cts.Cancel(); + await Assert.ThrowsAsync(() => responseTask); + Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5))); + // It can take several tries before Send notices the disconnect. + for (int i = 0; i < 100; i++) + { + await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); + } + context.Dispose(); + } + } + + [Fact] + public async Task ResponseSendFileExceptions_ClientDisconnectsBeforeSecondSend_SendThrows() + { + string address; + using (var server = Utilities.CreateHttpServer(out address)) + { + server.Settings.ThrowWriteExceptions = true; + RequestContext context; + using (var client = new HttpClient()) + { + var responseTask = client.GetAsync(address, HttpCompletionOption.ResponseHeadersRead); + + context = await server.AcceptAsync(); + // First write sends headers + await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); + + var response = await responseTask; + response.EnsureSuccessStatusCode(); + response.Dispose(); + } + + Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5))); + await Assert.ThrowsAsync(async () => + { + // It can take several tries before Write notices the disconnect. + for (int i = 0; i < 100; i++) + { + await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); + } + }); + context.Dispose(); + } + } + + [Fact] + public async Task ResponseSendFile_ClientDisconnectsBeforeSecondSend_SendCompletesSilently() + { + string address; + using (var server = Utilities.CreateHttpServer(out address)) + { + RequestContext context; + using (var client = new HttpClient()) + { + var responseTask = client.GetAsync(address, HttpCompletionOption.ResponseHeadersRead); + + context = await server.AcceptAsync(); + // First write sends headers + await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); + + var response = await responseTask; + response.EnsureSuccessStatusCode(); + response.Dispose(); + } + + Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5))); + // It can take several tries before Write notices the disconnect. + for (int i = 0; i < 10; i++) + { + await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); + } + context.Dispose(); + } + } + + private async Task SendRequestAsync(string uri, CancellationToken cancellationToken = new CancellationToken()) { using (HttpClient client = new HttpClient()) { - return await client.GetAsync(uri); + return await client.GetAsync(uri, cancellationToken); } } }