#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)
|
||||
{
|
||||
if (logger == null)
|
||||
|
|
@ -61,7 +73,7 @@ namespace Microsoft.Net.Http.Server
|
|||
}
|
||||
else
|
||||
{
|
||||
logger.LogError(location, exception);
|
||||
logger.LogError(0, exception, location);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue