#173 Ignore write failures, fix disconnect notifications.

This commit is contained in:
Chris R 2016-08-22 10:53:59 -07:00
parent efef52a0ad
commit d8209b6cd4
7 changed files with 749 additions and 102 deletions

View File

@ -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);
}
}

View File

@ -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

View File

@ -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<byte> 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<int>();
}
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<byte>(buffer, offset, count);
if (cancellationToken.IsCancellationRequested)
{
return Helpers.CanceledTask<int>();
}
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<int>();
}
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);
}

View File

@ -41,18 +41,26 @@ namespace Microsoft.Net.Http.Server
private ResponseStream _responseStream;
private TaskCompletionSource<object> _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<object>();
var cancellationRegistration = default(CancellationTokenRegistration);
if (cancellationToken.CanBeCanceled)
{
cancellationRegistration = _responseStream.RequestContext.RegisterForCancellation(cancellationToken);
}
_cancellationToken = cancellationToken;
_cancellationRegistration = cancellationRegistration;
}
internal ResponseStreamAsyncResult(ResponseStream responseStream, ArraySegment<byte> 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

View File

@ -69,13 +69,11 @@ namespace Microsoft.Net.Http.Server
/// </summary>
public TimeoutManager Timeouts { get; } = new TimeoutManager();
// TODO: https://github.com/aspnet/WebListener/issues/173
/// <summary>
/// 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.
/// </summary>
internal bool IgnoreWriteExceptions { get; set; } = true;
public bool ThrowWriteExceptions { get; set; }
/// <summary>
/// Gets or sets the maximum number of requests that will be queued up in Http.Sys.

View File

@ -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<HttpResponseMessage> 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<string> ignored;
@ -44,7 +43,7 @@ namespace Microsoft.Net.Http.Server
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
Task<HttpResponseMessage> 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<string> 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<HttpResponseMessage> 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<string> ignored;
@ -92,11 +91,11 @@ namespace Microsoft.Net.Http.Server
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
Task<HttpResponseMessage> 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<string> contentLength;
@ -123,7 +122,7 @@ namespace Microsoft.Net.Http.Server
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
Task<HttpResponseMessage> 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<HttpResponseMessage> 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<HttpResponseMessage> 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<HttpResponseMessage> 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<HttpResponseMessage> 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<string> ignored;
@ -228,7 +227,7 @@ namespace Microsoft.Net.Http.Server
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
Task<HttpResponseMessage> 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<HttpResponseMessage> 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<HttpResponseMessage> 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<HttpRequestException>(() => responseTask);
}
}
[Fact]
public async Task ResponseBody_SecondWriteAsyncWithCanceledCancellationToken_CancelsButDoesNotAbort()
public async Task ResponseBody_FirstWriteAsyncWithCanceledCancellationToken_CancelsAndAborts()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
Task<HttpResponseMessage> 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<HttpRequestException>(() => 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<HttpRequestException>(() => responseTask);
}
}
private async Task<HttpResponseMessage> 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<HttpRequestException>(() => 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<TaskCanceledException>(() => responseTask);
Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5)));
Assert.Throws<IOException>(() =>
{
// 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<ObjectDisposedException>(() => 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<TaskCanceledException>(() => responseTask);
Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5)));
await Assert.ThrowsAsync<IOException>(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<ObjectDisposedException>(() => 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<TaskCanceledException>(() => 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<TaskCanceledException>(() => 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<IOException>(() =>
{
// 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<IOException>(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<HttpResponseMessage> SendRequestAsync(string uri, CancellationToken cancellationToken = new CancellationToken())
{
using (HttpClient client = new HttpClient())
{
return await client.GetAsync(uri);
return await client.GetAsync(uri, cancellationToken);
}
}
}

View File

@ -298,11 +298,276 @@ namespace Microsoft.Net.Http.Server
}
}
private async Task<HttpResponseMessage> 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<HttpRequestException>(() => 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<HttpRequestException>(() => 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<HttpRequestException>(() => 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<HttpRequestException>(() => 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<TaskCanceledException>(() => responseTask);
Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5)));
await Assert.ThrowsAsync<IOException>(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<ObjectDisposedException>(() =>
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<TaskCanceledException>(() => 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<IOException>(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<HttpResponseMessage> SendRequestAsync(string uri, CancellationToken cancellationToken = new CancellationToken())
{
using (HttpClient client = new HttpClient())
{
return await client.GetAsync(uri);
return await client.GetAsync(uri, cancellationToken);
}
}
}