#173 Ignore write failures, fix disconnect notifications.
This commit is contained in:
parent
efef52a0ad
commit
d8209b6cd4
|
|
@ -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)
|
internal static void LogException(ILogger logger, string location, Exception exception)
|
||||||
{
|
{
|
||||||
if (logger == null)
|
if (logger == null)
|
||||||
|
|
@ -61,7 +73,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.LogError(location, exception);
|
logger.LogError(0, exception, location);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
|
@ -75,6 +76,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
// Create a nativeOverlapped callback so we can register for disconnect callback
|
// Create a nativeOverlapped callback so we can register for disconnect callback
|
||||||
var cts = new CancellationTokenSource();
|
var cts = new CancellationTokenSource();
|
||||||
|
var returnToken = cts.Token;
|
||||||
|
|
||||||
SafeNativeOverlapped nativeOverlapped = null;
|
SafeNativeOverlapped nativeOverlapped = null;
|
||||||
var boundHandle = _requestQueue.BoundHandle;
|
var boundHandle = _requestQueue.BoundHandle;
|
||||||
|
|
@ -97,8 +99,6 @@ namespace Microsoft.Net.Http.Server
|
||||||
{
|
{
|
||||||
LogHelper.LogException(_logger, "CreateDisconnectToken Callback", exception);
|
LogHelper.LogException(_logger, "CreateDisconnectToken Callback", exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
cts.Dispose();
|
|
||||||
},
|
},
|
||||||
null, null));
|
null, null));
|
||||||
|
|
||||||
|
|
@ -117,19 +117,24 @@ namespace Microsoft.Net.Http.Server
|
||||||
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING &&
|
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING &&
|
||||||
statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
|
statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
|
||||||
{
|
{
|
||||||
// We got an unknown result so return a None
|
// We got an unknown result, assume the connection has been closed.
|
||||||
// TODO: return a canceled token?
|
nativeOverlapped.Dispose();
|
||||||
return CancellationToken.None;
|
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)
|
if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess)
|
||||||
{
|
{
|
||||||
// IO operation completed synchronously - callback won't be called to signal completion.
|
// IO operation completed synchronously - callback won't be called to signal completion
|
||||||
// TODO: return a canceled token?
|
nativeOverlapped.Dispose();
|
||||||
return CancellationToken.None;
|
ConnectionCancellation ignored;
|
||||||
|
_connectionCancellationTokens.TryRemove(connectionId, out ignored);
|
||||||
|
cts.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
return cts.Token;
|
return returnToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ConnectionCancellation
|
private class ConnectionCancellation
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@ namespace Microsoft.Net.Http.Server
|
||||||
{
|
{
|
||||||
private RequestContext _requestContext;
|
private RequestContext _requestContext;
|
||||||
private long _leftToWrite = long.MinValue;
|
private long _leftToWrite = long.MinValue;
|
||||||
private bool _closed;
|
private bool _skipWrites;
|
||||||
|
private bool _disposed;
|
||||||
private bool _inOpaqueMode;
|
private bool _inOpaqueMode;
|
||||||
|
|
||||||
// The last write needs special handling to cancel.
|
// The last write needs special handling to cancel.
|
||||||
|
|
@ -60,6 +61,8 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
private ILogger Logger => RequestContext.Server.Logger;
|
private ILogger Logger => RequestContext.Server.Logger;
|
||||||
|
|
||||||
|
internal bool ThrowWriteExceptions => RequestContext.Server.Settings.ThrowWriteExceptions;
|
||||||
|
|
||||||
public override bool CanSeek
|
public override bool CanSeek
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
@ -107,7 +110,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
// Send headers
|
// Send headers
|
||||||
public override void Flush()
|
public override void Flush()
|
||||||
{
|
{
|
||||||
if (_closed)
|
if (_disposed)
|
||||||
{
|
{
|
||||||
return;
|
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.");
|
Debug.Assert(!(endOfRequest && data.Count > 0), "Data is not supported at the end of the request.");
|
||||||
|
|
||||||
|
if (_skipWrites)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var started = _requestContext.Response.HasStarted;
|
var started = _requestContext.Response.HasStarted;
|
||||||
if (data.Count == 0 && started && !endOfRequest)
|
if (data.Count == 0 && started && !endOfRequest)
|
||||||
{
|
{
|
||||||
|
|
@ -170,11 +178,6 @@ namespace Microsoft.Net.Http.Server
|
||||||
SafeNativeOverlapped.Zero,
|
SafeNativeOverlapped.Zero,
|
||||||
IntPtr.Zero);
|
IntPtr.Zero);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_requestContext.Server.Settings.IgnoreWriteExceptions)
|
|
||||||
{
|
|
||||||
statusCode = UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
@ -186,10 +189,19 @@ namespace Microsoft.Net.Http.Server
|
||||||
// Don't throw for disconnects, we were already finished with the response.
|
// Don't throw for disconnects, we were already finished with the response.
|
||||||
&& (!endOfRequest || (statusCode != ErrorCodes.ERROR_CONNECTION_INVALID && statusCode != ErrorCodes.ERROR_INVALID_PARAMETER)))
|
&& (!endOfRequest || (statusCode != ErrorCodes.ERROR_CONNECTION_INVALID && statusCode != ErrorCodes.ERROR_INVALID_PARAMETER)))
|
||||||
{
|
{
|
||||||
Exception exception = new IOException(string.Empty, new WebListenerException((int)statusCode));
|
if (ThrowWriteExceptions)
|
||||||
LogHelper.LogException(Logger, "Flush", exception);
|
{
|
||||||
Abort();
|
var exception = new IOException(string.Empty, new WebListenerException((int)statusCode));
|
||||||
throw exception;
|
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)
|
public override Task FlushAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (_closed)
|
if (_disposed)
|
||||||
{
|
{
|
||||||
return Helpers.CompletedTask();
|
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.
|
// 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)
|
private unsafe Task FlushInternalAsync(ArraySegment<byte> data, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
if (_skipWrites)
|
||||||
|
{
|
||||||
|
return Helpers.CompletedTask();
|
||||||
|
}
|
||||||
|
|
||||||
var started = _requestContext.Response.HasStarted;
|
var started = _requestContext.Response.HasStarted;
|
||||||
if (data.Count == 0 && started)
|
if (data.Count == 0 && started)
|
||||||
{
|
{
|
||||||
|
|
@ -288,10 +305,10 @@ namespace Microsoft.Net.Http.Server
|
||||||
return Helpers.CompletedTask();
|
return Helpers.CompletedTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
var cancellationRegistration = default(CancellationTokenRegistration);
|
if (cancellationToken.IsCancellationRequested)
|
||||||
if (cancellationToken.CanBeCanceled)
|
|
||||||
{
|
{
|
||||||
cancellationRegistration = RequestContext.RegisterForCancellation(cancellationToken);
|
Abort(ThrowWriteExceptions);
|
||||||
|
return Helpers.CanceledTask<int>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var flags = ComputeLeftToWrite();
|
var flags = ComputeLeftToWrite();
|
||||||
|
|
@ -303,7 +320,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
UpdateWritenCount((uint)data.Count);
|
UpdateWritenCount((uint)data.Count);
|
||||||
uint statusCode = 0;
|
uint statusCode = 0;
|
||||||
var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked;
|
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;
|
uint bytesSent = 0;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -337,18 +354,25 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
if (statusCode != ErrorCodes.ERROR_SUCCESS && statusCode != ErrorCodes.ERROR_IO_PENDING)
|
if (statusCode != ErrorCodes.ERROR_SUCCESS && statusCode != ErrorCodes.ERROR_IO_PENDING)
|
||||||
{
|
{
|
||||||
asyncResult.Dispose();
|
if (cancellationToken.IsCancellationRequested)
|
||||||
if (_requestContext.Server.Settings.IgnoreWriteExceptions && started)
|
|
||||||
{
|
{
|
||||||
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));
|
Exception exception = new IOException(string.Empty, new WebListenerException((int)statusCode));
|
||||||
LogHelper.LogException(Logger, "FlushAsync", exception);
|
LogHelper.LogException(Logger, "FlushAsync", exception);
|
||||||
Abort();
|
Abort();
|
||||||
throw exception;
|
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)
|
if (statusCode == ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess)
|
||||||
|
|
@ -397,9 +421,16 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
internal void Abort()
|
internal void Abort(bool dispose = true)
|
||||||
{
|
{
|
||||||
_closed = true;
|
if (dispose)
|
||||||
|
{
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_skipWrites = true;
|
||||||
|
}
|
||||||
_requestContext.Abort();
|
_requestContext.Abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -476,14 +507,10 @@ namespace Microsoft.Net.Http.Server
|
||||||
((Task)asyncResult).GetAwaiter().GetResult();
|
((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.
|
// Validates for null and bounds. Allows count == 0.
|
||||||
var data = new ArraySegment<byte>(buffer, offset, count);
|
var data = new ArraySegment<byte>(buffer, offset, count);
|
||||||
if (cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
return Helpers.CanceledTask<int>();
|
|
||||||
}
|
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
// TODO: Verbose log parameters
|
// 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)
|
internal unsafe Task SendFileAsyncCore(string fileName, long offset, long? count, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
if (_skipWrites)
|
||||||
|
{
|
||||||
|
return Helpers.CompletedTask();
|
||||||
|
}
|
||||||
|
|
||||||
var flags = ComputeLeftToWrite();
|
var flags = ComputeLeftToWrite();
|
||||||
if (count == 0 && _leftToWrite != 0)
|
if (count == 0 && _leftToWrite != 0)
|
||||||
{
|
{
|
||||||
return Helpers.CompletedTask();
|
return Helpers.CompletedTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_leftToWrite >= 0 && count > _leftToWrite)
|
if (_leftToWrite >= 0 && count > _leftToWrite)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(Resources.Exception_TooMuchWritten);
|
throw new InvalidOperationException(Resources.Exception_TooMuchWritten);
|
||||||
}
|
}
|
||||||
// TODO: Verbose log
|
|
||||||
|
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
|
Abort(ThrowWriteExceptions);
|
||||||
return Helpers.CanceledTask<int>();
|
return Helpers.CanceledTask<int>();
|
||||||
}
|
}
|
||||||
|
// TODO: Verbose log
|
||||||
var cancellationRegistration = default(CancellationTokenRegistration);
|
|
||||||
if (cancellationToken.CanBeCanceled)
|
|
||||||
{
|
|
||||||
cancellationRegistration = RequestContext.RegisterForCancellation(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint statusCode;
|
uint statusCode;
|
||||||
uint bytesSent = 0;
|
uint bytesSent = 0;
|
||||||
var started = _requestContext.Response.HasStarted;
|
var started = _requestContext.Response.HasStarted;
|
||||||
var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked;
|
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;
|
long bytesWritten;
|
||||||
if (chunked)
|
if (chunked)
|
||||||
|
|
@ -601,18 +629,25 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
|
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
|
||||||
{
|
{
|
||||||
asyncResult.Dispose();
|
if (cancellationToken.IsCancellationRequested)
|
||||||
if (_requestContext.Server.Settings.IgnoreWriteExceptions && started)
|
|
||||||
{
|
{
|
||||||
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);
|
LogHelper.LogException(Logger, "SendFileAsync", exception);
|
||||||
Abort();
|
Abort();
|
||||||
throw exception;
|
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)
|
if (statusCode == ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess)
|
||||||
|
|
@ -642,7 +677,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
if (_leftToWrite == 0)
|
if (_leftToWrite == 0)
|
||||||
{
|
{
|
||||||
// in this case we already passed 0 as the flag, so we don't need to call HttpSendResponseEntityBody() when we Close()
|
// 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 (disposing)
|
||||||
{
|
{
|
||||||
if (_closed)
|
if (_disposed)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_closed = true;
|
_disposed = true;
|
||||||
FlushInternal(endOfRequest: true);
|
FlushInternal(endOfRequest: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -688,7 +723,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
private void CheckDisposed()
|
private void CheckDisposed()
|
||||||
{
|
{
|
||||||
if (_closed)
|
if (_disposed)
|
||||||
{
|
{
|
||||||
throw new ObjectDisposedException(GetType().FullName);
|
throw new ObjectDisposedException(GetType().FullName);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,18 +41,26 @@ namespace Microsoft.Net.Http.Server
|
||||||
private ResponseStream _responseStream;
|
private ResponseStream _responseStream;
|
||||||
private TaskCompletionSource<object> _tcs;
|
private TaskCompletionSource<object> _tcs;
|
||||||
private uint _bytesSent;
|
private uint _bytesSent;
|
||||||
|
private CancellationToken _cancellationToken;
|
||||||
private CancellationTokenRegistration _cancellationRegistration;
|
private CancellationTokenRegistration _cancellationRegistration;
|
||||||
|
|
||||||
internal ResponseStreamAsyncResult(ResponseStream responseStream, CancellationTokenRegistration cancellationRegistration)
|
internal ResponseStreamAsyncResult(ResponseStream responseStream, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_responseStream = responseStream;
|
_responseStream = responseStream;
|
||||||
_tcs = new TaskCompletionSource<object>();
|
_tcs = new TaskCompletionSource<object>();
|
||||||
|
|
||||||
|
var cancellationRegistration = default(CancellationTokenRegistration);
|
||||||
|
if (cancellationToken.CanBeCanceled)
|
||||||
|
{
|
||||||
|
cancellationRegistration = _responseStream.RequestContext.RegisterForCancellation(cancellationToken);
|
||||||
|
}
|
||||||
|
_cancellationToken = cancellationToken;
|
||||||
_cancellationRegistration = cancellationRegistration;
|
_cancellationRegistration = cancellationRegistration;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal ResponseStreamAsyncResult(ResponseStream responseStream, ArraySegment<byte> data, bool chunked,
|
internal ResponseStreamAsyncResult(ResponseStream responseStream, ArraySegment<byte> data, bool chunked,
|
||||||
CancellationTokenRegistration cancellationRegistration)
|
CancellationToken cancellationToken)
|
||||||
: this(responseStream, cancellationRegistration)
|
: this(responseStream, cancellationToken)
|
||||||
{
|
{
|
||||||
var boundHandle = _responseStream.RequestContext.Server.RequestQueue.BoundHandle;
|
var boundHandle = _responseStream.RequestContext.Server.RequestQueue.BoundHandle;
|
||||||
object[] objectsToPin;
|
object[] objectsToPin;
|
||||||
|
|
@ -107,8 +115,8 @@ namespace Microsoft.Net.Http.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
internal ResponseStreamAsyncResult(ResponseStream responseStream, string fileName, long offset,
|
internal ResponseStreamAsyncResult(ResponseStream responseStream, string fileName, long offset,
|
||||||
long? count, bool chunked, CancellationTokenRegistration cancellationRegistration)
|
long? count, bool chunked, CancellationToken cancellationToken)
|
||||||
: this(responseStream, cancellationRegistration)
|
: this(responseStream, cancellationToken)
|
||||||
{
|
{
|
||||||
var boundHandle = responseStream.RequestContext.Server.RequestQueue.BoundHandle;
|
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")]
|
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Redirecting to callback")]
|
||||||
private static void IOCompleted(ResponseStreamAsyncResult asyncResult, uint errorCode, uint numBytes)
|
private static void IOCompleted(ResponseStreamAsyncResult asyncResult, uint errorCode, uint numBytes)
|
||||||
{
|
{
|
||||||
|
var logger = asyncResult._responseStream.RequestContext.Logger;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF)
|
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
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -285,6 +309,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
LogHelper.LogException(logger, "FlushAsync.IOCompleted", e);
|
||||||
asyncResult.Fail(e);
|
asyncResult.Fail(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -297,15 +322,30 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
internal void Complete()
|
internal void Complete()
|
||||||
{
|
{
|
||||||
_tcs.TrySetResult(null);
|
|
||||||
Dispose();
|
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)
|
internal void Fail(Exception ex)
|
||||||
{
|
{
|
||||||
_tcs.TrySetException(ex);
|
|
||||||
Dispose();
|
Dispose();
|
||||||
_responseStream.Abort();
|
_responseStream.Abort();
|
||||||
|
_tcs.TrySetException(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
public object AsyncState
|
public object AsyncState
|
||||||
|
|
|
||||||
|
|
@ -69,13 +69,11 @@ namespace Microsoft.Net.Http.Server
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeoutManager Timeouts { get; } = new TimeoutManager();
|
public TimeoutManager Timeouts { get; } = new TimeoutManager();
|
||||||
|
|
||||||
|
|
||||||
// TODO: https://github.com/aspnet/WebListener/issues/173
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or Sets if response body writes that fail due to client disconnects should throw exceptions or
|
/// 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>
|
/// </summary>
|
||||||
internal bool IgnoreWriteExceptions { get; set; } = true;
|
public bool ThrowWriteExceptions { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the maximum number of requests that will be queued up in Http.Sys.
|
/// Gets or sets the maximum number of requests that will be queued up in Http.Sys.
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Testing.xunit;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.Net.Http.Server
|
namespace Microsoft.Net.Http.Server
|
||||||
|
|
@ -21,14 +20,14 @@ namespace Microsoft.Net.Http.Server
|
||||||
string address;
|
string address;
|
||||||
using (var server = Utilities.CreateHttpServer(out address))
|
using (var server = Utilities.CreateHttpServer(out address))
|
||||||
{
|
{
|
||||||
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
var responseTask = SendRequestAsync(address);
|
||||||
|
|
||||||
var context = await server.AcceptAsync();
|
var context = await server.AcceptAsync();
|
||||||
context.Response.Body.Write(new byte[10], 0, 10);
|
context.Response.Body.Write(new byte[10], 0, 10);
|
||||||
await context.Response.Body.WriteAsync(new byte[10], 0, 10);
|
await context.Response.Body.WriteAsync(new byte[10], 0, 10);
|
||||||
context.Dispose();
|
context.Dispose();
|
||||||
|
|
||||||
HttpResponseMessage response = await responseTask;
|
var response = await responseTask;
|
||||||
Assert.Equal(200, (int)response.StatusCode);
|
Assert.Equal(200, (int)response.StatusCode);
|
||||||
Assert.Equal(new Version(1, 1), response.Version);
|
Assert.Equal(new Version(1, 1), response.Version);
|
||||||
IEnumerable<string> ignored;
|
IEnumerable<string> ignored;
|
||||||
|
|
@ -44,7 +43,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
string address;
|
string address;
|
||||||
using (var server = Utilities.CreateHttpServer(out address))
|
using (var server = Utilities.CreateHttpServer(out address))
|
||||||
{
|
{
|
||||||
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
var responseTask = SendRequestAsync(address);
|
||||||
|
|
||||||
var context = await server.AcceptAsync();
|
var context = await server.AcceptAsync();
|
||||||
context.Response.Body.Write(new byte[10], 0, 10);
|
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);
|
await context.Response.Body.WriteAsync(new byte[10], 0, 10);
|
||||||
context.Dispose();
|
context.Dispose();
|
||||||
|
|
||||||
HttpResponseMessage response = await responseTask;
|
var response = await responseTask;
|
||||||
Assert.Equal(200, (int)response.StatusCode);
|
Assert.Equal(200, (int)response.StatusCode);
|
||||||
IEnumerable<string> contentLength;
|
IEnumerable<string> contentLength;
|
||||||
Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
|
Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
|
||||||
|
|
@ -67,7 +66,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
string address;
|
string address;
|
||||||
using (var server = Utilities.CreateHttpServer(out address))
|
using (var server = Utilities.CreateHttpServer(out address))
|
||||||
{
|
{
|
||||||
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
var responseTask = SendRequestAsync(address);
|
||||||
|
|
||||||
var context = await server.AcceptAsync();
|
var context = await server.AcceptAsync();
|
||||||
context.Response.Headers["transfeR-Encoding"] = "CHunked";
|
context.Response.Headers["transfeR-Encoding"] = "CHunked";
|
||||||
|
|
@ -76,7 +75,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
await stream.WriteAsync(responseBytes, 0, responseBytes.Length);
|
await stream.WriteAsync(responseBytes, 0, responseBytes.Length);
|
||||||
context.Dispose();
|
context.Dispose();
|
||||||
|
|
||||||
HttpResponseMessage response = await responseTask;
|
var response = await responseTask;
|
||||||
Assert.Equal(200, (int)response.StatusCode);
|
Assert.Equal(200, (int)response.StatusCode);
|
||||||
Assert.Equal(new Version(1, 1), response.Version);
|
Assert.Equal(new Version(1, 1), response.Version);
|
||||||
IEnumerable<string> ignored;
|
IEnumerable<string> ignored;
|
||||||
|
|
@ -92,11 +91,11 @@ namespace Microsoft.Net.Http.Server
|
||||||
string address;
|
string address;
|
||||||
using (var server = Utilities.CreateHttpServer(out address))
|
using (var server = Utilities.CreateHttpServer(out address))
|
||||||
{
|
{
|
||||||
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
var responseTask = SendRequestAsync(address);
|
||||||
|
|
||||||
var context = await server.AcceptAsync();
|
var context = await server.AcceptAsync();
|
||||||
context.Response.Headers["Content-lenGth"] = " 30 ";
|
context.Response.Headers["Content-lenGth"] = " 30 ";
|
||||||
Stream stream = context.Response.Body;
|
var stream = context.Response.Body;
|
||||||
#if NET451
|
#if NET451
|
||||||
stream.EndWrite(stream.BeginWrite(new byte[10], 0, 10, null, null));
|
stream.EndWrite(stream.BeginWrite(new byte[10], 0, 10, null, null));
|
||||||
#else
|
#else
|
||||||
|
|
@ -106,7 +105,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
await stream.WriteAsync(new byte[10], 0, 10);
|
await stream.WriteAsync(new byte[10], 0, 10);
|
||||||
context.Dispose();
|
context.Dispose();
|
||||||
|
|
||||||
HttpResponseMessage response = await responseTask;
|
var response = await responseTask;
|
||||||
Assert.Equal(200, (int)response.StatusCode);
|
Assert.Equal(200, (int)response.StatusCode);
|
||||||
Assert.Equal(new Version(1, 1), response.Version);
|
Assert.Equal(new Version(1, 1), response.Version);
|
||||||
IEnumerable<string> contentLength;
|
IEnumerable<string> contentLength;
|
||||||
|
|
@ -123,7 +122,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
string address;
|
string address;
|
||||||
using (var server = Utilities.CreateHttpServer(out address))
|
using (var server = Utilities.CreateHttpServer(out address))
|
||||||
{
|
{
|
||||||
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
var responseTask = SendRequestAsync(address);
|
||||||
|
|
||||||
var context = await server.AcceptAsync();
|
var context = await server.AcceptAsync();
|
||||||
context.Response.Headers["Content-lenGth"] = " 20 ";
|
context.Response.Headers["Content-lenGth"] = " 20 ";
|
||||||
|
|
@ -144,7 +143,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
string address;
|
string address;
|
||||||
using (var server = Utilities.CreateHttpServer(out address))
|
using (var server = Utilities.CreateHttpServer(out address))
|
||||||
{
|
{
|
||||||
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
var responseTask = SendRequestAsync(address);
|
||||||
|
|
||||||
var context = await server.AcceptAsync();
|
var context = await server.AcceptAsync();
|
||||||
context.Response.Headers["Content-lenGth"] = " 20 ";
|
context.Response.Headers["Content-lenGth"] = " 20 ";
|
||||||
|
|
@ -161,7 +160,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
string address;
|
string address;
|
||||||
using (var server = Utilities.CreateHttpServer(out address))
|
using (var server = Utilities.CreateHttpServer(out address))
|
||||||
{
|
{
|
||||||
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
var responseTask = SendRequestAsync(address);
|
||||||
|
|
||||||
var context = await server.AcceptAsync();
|
var context = await server.AcceptAsync();
|
||||||
context.Response.Headers["Content-lenGth"] = " 10 ";
|
context.Response.Headers["Content-lenGth"] = " 10 ";
|
||||||
|
|
@ -179,7 +178,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
string address;
|
string address;
|
||||||
using (var server = Utilities.CreateHttpServer(out address))
|
using (var server = Utilities.CreateHttpServer(out address))
|
||||||
{
|
{
|
||||||
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
var responseTask = SendRequestAsync(address);
|
||||||
|
|
||||||
var context = await server.AcceptAsync();
|
var context = await server.AcceptAsync();
|
||||||
context.Response.Headers["Content-lenGth"] = " 10 ";
|
context.Response.Headers["Content-lenGth"] = " 10 ";
|
||||||
|
|
@ -204,7 +203,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
string address;
|
string address;
|
||||||
using (var server = Utilities.CreateHttpServer(out address))
|
using (var server = Utilities.CreateHttpServer(out address))
|
||||||
{
|
{
|
||||||
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
var responseTask = SendRequestAsync(address);
|
||||||
|
|
||||||
var context = await server.AcceptAsync();
|
var context = await server.AcceptAsync();
|
||||||
context.Response.Body.Write(new byte[10], 0, 0);
|
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);
|
await context.Response.Body.WriteAsync(new byte[10], 0, 0);
|
||||||
context.Dispose();
|
context.Dispose();
|
||||||
|
|
||||||
HttpResponseMessage response = await responseTask;
|
var response = await responseTask;
|
||||||
Assert.Equal(200, (int)response.StatusCode);
|
Assert.Equal(200, (int)response.StatusCode);
|
||||||
Assert.Equal(new Version(1, 1), response.Version);
|
Assert.Equal(new Version(1, 1), response.Version);
|
||||||
IEnumerable<string> ignored;
|
IEnumerable<string> ignored;
|
||||||
|
|
@ -228,7 +227,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
string address;
|
string address;
|
||||||
using (var server = Utilities.CreateHttpServer(out address))
|
using (var server = Utilities.CreateHttpServer(out address))
|
||||||
{
|
{
|
||||||
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
var responseTask = SendRequestAsync(address);
|
||||||
|
|
||||||
var context = await server.AcceptAsync();
|
var context = await server.AcceptAsync();
|
||||||
var cts = new CancellationTokenSource();
|
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);
|
await context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token);
|
||||||
context.Dispose();
|
context.Dispose();
|
||||||
|
|
||||||
HttpResponseMessage response = await responseTask;
|
var response = await responseTask;
|
||||||
Assert.Equal(200, (int)response.StatusCode);
|
Assert.Equal(200, (int)response.StatusCode);
|
||||||
Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
|
Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
|
||||||
}
|
}
|
||||||
|
|
@ -249,29 +248,30 @@ namespace Microsoft.Net.Http.Server
|
||||||
string address;
|
string address;
|
||||||
using (var server = Utilities.CreateHttpServer(out address))
|
using (var server = Utilities.CreateHttpServer(out address))
|
||||||
{
|
{
|
||||||
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
var responseTask = SendRequestAsync(address);
|
||||||
|
|
||||||
var context = await server.AcceptAsync();
|
var context = await server.AcceptAsync();
|
||||||
var cts = new CancellationTokenSource();
|
var cts = new CancellationTokenSource();
|
||||||
cts.CancelAfter(TimeSpan.FromSeconds(1));
|
cts.CancelAfter(TimeSpan.FromSeconds(10));
|
||||||
// First write sends headers
|
// 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);
|
||||||
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();
|
context.Dispose();
|
||||||
|
|
||||||
HttpResponseMessage response = await responseTask;
|
var response = await responseTask;
|
||||||
Assert.Equal(200, (int)response.StatusCode);
|
Assert.Equal(200, (int)response.StatusCode);
|
||||||
Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
|
Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ResponseBody_FirstWriteAsyncWithCanceledCancellationToken_CancelsButDoesNotAbort()
|
public async Task ResponseBodyWriteExceptions_FirstWriteAsyncWithCanceledCancellationToken_CancelsAndAborts()
|
||||||
{
|
{
|
||||||
string address;
|
string address;
|
||||||
using (var server = Utilities.CreateHttpServer(out 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 context = await server.AcceptAsync();
|
||||||
var cts = new CancellationTokenSource();
|
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);
|
var writeTask = context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token);
|
||||||
Assert.True(writeTask.IsCanceled);
|
Assert.True(writeTask.IsCanceled);
|
||||||
context.Dispose();
|
context.Dispose();
|
||||||
|
#if NET451
|
||||||
HttpResponseMessage response = await responseTask;
|
// .NET 4.5 HttpClient automatically retries a request if it does not get a response.
|
||||||
Assert.Equal(200, (int)response.StatusCode);
|
context = await server.AcceptAsync();
|
||||||
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
|
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]
|
[Fact]
|
||||||
public async Task ResponseBody_SecondWriteAsyncWithCanceledCancellationToken_CancelsButDoesNotAbort()
|
public async Task ResponseBody_FirstWriteAsyncWithCanceledCancellationToken_CancelsAndAborts()
|
||||||
{
|
{
|
||||||
string address;
|
string address;
|
||||||
using (var server = Utilities.CreateHttpServer(out 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 context = await server.AcceptAsync();
|
||||||
var cts = new CancellationTokenSource();
|
var cts = new CancellationTokenSource();
|
||||||
|
|
@ -304,17 +341,272 @@ namespace Microsoft.Net.Http.Server
|
||||||
Assert.True(writeTask.IsCanceled);
|
Assert.True(writeTask.IsCanceled);
|
||||||
context.Dispose();
|
context.Dispose();
|
||||||
|
|
||||||
HttpResponseMessage response = await responseTask;
|
await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
|
||||||
Assert.Equal(200, (int)response.StatusCode);
|
|
||||||
Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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())
|
using (HttpClient client = new HttpClient())
|
||||||
{
|
{
|
||||||
return await client.GetAsync(uri);
|
return await client.GetAsync(uri, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())
|
using (HttpClient client = new HttpClient())
|
||||||
{
|
{
|
||||||
return await client.GetAsync(uri);
|
return await client.GetAsync(uri, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue