Reorder SendFileAsync to match Write/FlushAsync

This commit is contained in:
Chris R 2016-08-26 10:10:09 -07:00
parent c2a1b8d17f
commit c2f52db3a5
8 changed files with 143 additions and 131 deletions

View File

@ -23,6 +23,7 @@
using System;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@ -81,6 +82,17 @@ namespace Microsoft.Net.Http.Server
return tcs.Task;
}
internal static ArraySegment<byte> GetChunkHeader(long size)
{
if (size < int.MaxValue)
{
return GetChunkHeader((int)size);
}
// Greater than 2gb, perf is no longer our concern
return new ArraySegment<byte>(Encoding.ASCII.GetBytes(size.ToString("X") + "\r\n"));
}
/// <summary>
/// A private utility routine to convert an integer to a chunk header,
/// which is an ASCII hex number followed by a CRLF.The header is returned

View File

@ -308,12 +308,13 @@ namespace Microsoft.Net.Http.Server
// TODO: Verbose log
try
{
if (_requestAbortSource != null)
{
_requestAbortSource.Dispose();
}
_requestAbortSource?.Dispose();
Response.Dispose();
}
catch
{
Abort();
}
finally
{
Request.Dispose();
@ -334,14 +335,19 @@ namespace Microsoft.Net.Http.Server
{
_requestAbortSource.Cancel();
}
catch (ObjectDisposedException)
{
}
catch (Exception ex)
{
LogHelper.LogException(Logger, "Abort", ex);
LogHelper.LogDebug(Logger, "Abort", ex);
}
_requestAbortSource.Dispose();
}
ForceCancelRequest();
Request.Dispose();
// Only Abort, Response.Dispose() tries a graceful flush
Response.Abort();
}
private static void Abort(object state)

View File

@ -109,7 +109,6 @@ namespace Microsoft.Net.Http.Server
{
get
{
CheckDisposed();
EnsureResponseStream();
return _nativeStream;
}
@ -245,6 +244,12 @@ namespace Microsoft.Net.Http.Server
}
}
internal void Abort()
{
// Update state for HasStarted. Do not attempt a graceful Dispose.
_responseState = ResponseState.Closed;
}
// should only be called from RequestContext
internal void Dispose()
{
@ -685,14 +690,6 @@ namespace Microsoft.Net.Http.Server
}
}
private void CheckDisposed()
{
if (_responseState >= ResponseState.Closed)
{
throw new ObjectDisposedException(GetType().FullName);
}
}
internal void CancelLastWrite()
{
_nativeStream?.CancelLastWrite();

View File

@ -132,12 +132,13 @@ namespace Microsoft.Net.Http.Server
var started = _requestContext.Response.HasStarted;
if (data.Count == 0 && started && !endOfRequest)
{
// Empty flush
// No data to send and we've already sent the headers
return;
}
// Make sure all validation is performed before this computes the headers
var flags = ComputeLeftToWrite(endOfRequest);
if (!_inOpaqueMode && endOfRequest && _leftToWrite > data.Count)
if (!_inOpaqueMode && endOfRequest && _leftToWrite > 0)
{
_requestContext.Abort();
// This is logged rather than thrown because it is too late for an exception to be visible in user code.
@ -303,7 +304,7 @@ namespace Microsoft.Net.Http.Server
var started = _requestContext.Response.HasStarted;
if (data.Count == 0 && started)
{
// Empty flush
// No data to send and we've already sent the headers
return Helpers.CompletedTask();
}
@ -313,6 +314,7 @@ namespace Microsoft.Net.Http.Server
return Helpers.CanceledTask<int>();
}
// Make sure all validation is performed before this computes the headers
var flags = ComputeLeftToWrite();
if (_leftToWrite != data.Count)
{
@ -464,30 +466,31 @@ namespace Microsoft.Net.Http.Server
public override void Write(byte[] buffer, int offset, int count)
{
// Validates for null and bounds. Allows count == 0.
// TODO: Verbose log parameters
var data = new ArraySegment<byte>(buffer, offset, count);
CheckDisposed();
// TODO: Verbose log parameters
var contentLength = _requestContext.Response.ContentLength;
if (contentLength.HasValue && !_requestContext.Response.HasComputedHeaders && contentLength.Value <= data.Count)
{
if (contentLength.Value < data.Count)
{
throw new InvalidOperationException("More bytes written than specified in the Content-Length header.");
}
}
// The last write in a response that has already started, flush immediately
else if (_requestContext.Response.HasComputedHeaders && _leftToWrite >= 0 && _leftToWrite <= data.Count)
{
if (_leftToWrite < data.Count)
{
throw new InvalidOperationException("More bytes written than specified in the Content-Length header.");
}
}
CheckWriteCount(count);
FlushInternal(endOfRequest: false, data: data);
}
private void CheckWriteCount(long? count)
{
var contentLength = _requestContext.Response.ContentLength;
// First write with more bytes written than the entire content-length
if (!_requestContext.Response.HasComputedHeaders && contentLength < count)
{
throw new InvalidOperationException("More bytes written than specified in the Content-Length header.");
}
// A write in a response that has already started where the count exceeds the remainder of the content-length
else if (_requestContext.Response.HasComputedHeaders && _requestContext.Response.BoundaryType == BoundaryType.ContentLength
&& _leftToWrite < count)
{
throw new InvalidOperationException("More bytes written than specified in the Content-Length header.");
}
}
#if NETSTANDARD1_3
public IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
#else
@ -512,26 +515,11 @@ namespace Microsoft.Net.Http.Server
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
// Validates for null and bounds. Allows count == 0.
// TODO: Verbose log parameters
var data = new ArraySegment<byte>(buffer, offset, count);
CheckDisposed();
// TODO: Verbose log parameters
var contentLength = _requestContext.Response.ContentLength;
if (contentLength.HasValue && !_requestContext.Response.HasComputedHeaders && contentLength.Value <= data.Count)
{
if (contentLength.Value < data.Count)
{
throw new InvalidOperationException("More bytes written than specified in the Content-Length header.");
}
}
// The last write in a response that has already started, flush immediately
else if (_requestContext.Response.HasComputedHeaders && _leftToWrite > 0 && _leftToWrite <= data.Count)
{
if (_leftToWrite < data.Count)
{
throw new InvalidOperationException("More bytes written than specified in the Content-Length header.");
}
}
CheckWriteCount(count);
return FlushInternalAsync(data, cancellationToken);
}
@ -540,12 +528,15 @@ namespace Microsoft.Net.Http.Server
{
// It's too expensive to validate the file attributes before opening the file. Open the file and then check the lengths.
// This all happens inside of ResponseStreamAsyncResult.
// TODO: Verbose log parameters
if (string.IsNullOrWhiteSpace(fileName))
{
throw new ArgumentNullException("fileName");
}
CheckDisposed();
CheckWriteCount(count);
// We can't mix await and unsafe so separate the unsafe code into another method.
await SendFileAsyncCore(fileName, offset, count, cancellationToken);
}
@ -557,43 +548,65 @@ namespace Microsoft.Net.Http.Server
return Helpers.CompletedTask();
}
var flags = ComputeLeftToWrite();
if (count == 0 && _leftToWrite != 0)
var started = _requestContext.Response.HasStarted;
if (count == 0 && started)
{
// No data to send and we've already sent the headers
return Helpers.CompletedTask();
}
if (_leftToWrite >= 0 && count > _leftToWrite)
{
throw new InvalidOperationException(Resources.Exception_TooMuchWritten);
}
if (cancellationToken.IsCancellationRequested)
{
Abort(ThrowWriteExceptions);
return Helpers.CanceledTask<int>();
}
// TODO: Verbose log
// We are setting buffer size to 1 to prevent FileStream from allocating it's internal buffer
// It's too expensive to validate anything before opening the file. Open the file and then check the lengths.
var fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, bufferSize: 1,
options: FileOptions.Asynchronous | FileOptions.SequentialScan); // Extremely expensive.
try
{
var length = fileStream.Length; // Expensive, only do it once
if (!count.HasValue)
{
count = length - offset;
}
if (offset < 0 || offset > length)
{
throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
}
if (count < 0 || count > length - offset)
{
throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty);
}
CheckWriteCount(count);
}
catch
{
fileStream.Dispose();
throw;
}
// Make sure all validation is performed before this computes the headers
var flags = ComputeLeftToWrite();
uint statusCode;
uint bytesSent = 0;
var started = _requestContext.Response.HasStarted;
var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked;
var asyncResult = new ResponseStreamAsyncResult(this, fileName, offset, count, chunked, cancellationToken);
var asyncResult = new ResponseStreamAsyncResult(this, fileStream, offset, count.Value, chunked, cancellationToken);
long bytesWritten;
if (chunked)
{
bytesWritten = 0;
}
else if (count.HasValue)
else
{
bytesWritten = count.Value;
}
else
{
bytesWritten = asyncResult.FileLength - offset;
}
// Update _leftToWrite now so we can queue up additional calls to SendFileAsync.
flags |= _leftToWrite == bytesWritten ? HttpApi.HTTP_FLAGS.NONE : HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
UpdateWritenCount((uint)bytesWritten);

View File

@ -114,33 +114,15 @@ namespace Microsoft.Net.Http.Server
}
}
internal ResponseStreamAsyncResult(ResponseStream responseStream, string fileName, long offset,
long? count, bool chunked, CancellationToken cancellationToken)
internal ResponseStreamAsyncResult(ResponseStream responseStream, FileStream fileStream, long offset,
long count, bool chunked, CancellationToken cancellationToken)
: this(responseStream, cancellationToken)
{
var boundHandle = responseStream.RequestContext.Server.RequestQueue.BoundHandle;
int bufferSize = 1024 * 64; // TODO: Validate buffer size choice.
#if NETSTANDARD1_3
_fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, bufferSize /*, useAsync: true*/); // Extremely expensive.
#else
// It's too expensive to validate anything before opening the file. Open the file and then check the lengths.
_fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, bufferSize,
FileOptions.Asynchronous | FileOptions.SequentialScan); // Extremely expensive.
#endif
long length = _fileStream.Length; // Expensive
if (offset < 0 || offset > length)
{
_fileStream.Dispose();
throw new ArgumentOutOfRangeException("offset", offset, string.Empty);
}
if (count.HasValue && (count < 0 || count > length - offset))
{
_fileStream.Dispose();
throw new ArgumentOutOfRangeException("count", count, string.Empty);
}
_fileStream = fileStream;
if (count == 0 || (!count.HasValue && _fileStream.Length == 0))
if (count == 0)
{
_dataChunks = null;
_overlapped = new SafeNativeOverlapped(boundHandle,
@ -156,14 +138,14 @@ namespace Microsoft.Net.Http.Server
var chunkHeaderBuffer = new ArraySegment<byte>();
if (chunked)
{
chunkHeaderBuffer = Helpers.GetChunkHeader((int)(count ?? _fileStream.Length - offset));
chunkHeaderBuffer = Helpers.GetChunkHeader(count);
_dataChunks[0].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
_dataChunks[0].fromMemory.BufferLength = (uint)chunkHeaderBuffer.Count;
objectsToPin[0] = chunkHeaderBuffer.Array;
_dataChunks[1].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromFileHandle;
_dataChunks[1].fromFile.offset = (ulong)offset;
_dataChunks[1].fromFile.count = (ulong)(count ?? -1);
_dataChunks[1].fromFile.count = (ulong)count;
_dataChunks[1].fromFile.fileHandle = _fileStream.SafeFileHandle.DangerousGetHandle();
// Nothing to pin for the file handle.
@ -175,7 +157,7 @@ namespace Microsoft.Net.Http.Server
{
_dataChunks[0].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromFileHandle;
_dataChunks[0].fromFile.offset = (ulong)offset;
_dataChunks[0].fromFile.count = (ulong)(count ?? -1);
_dataChunks[0].fromFile.count = (ulong)count;
_dataChunks[0].fromFile.fileHandle = _fileStream.SafeFileHandle.DangerousGetHandle();
}
@ -248,11 +230,6 @@ namespace Microsoft.Net.Http.Server
}
}
internal long FileLength
{
get { return _fileStream == null ? 0 : _fileStream.Length; }
}
internal bool EndCalled { get; set; }
internal void IOCompleted(uint errorCode)

View File

@ -40,7 +40,7 @@ namespace Microsoft.Net.Http.Server
// returned from HttpReceiveClientCertificate when using the
// FileCompletionNotificationModes.SkipCompletionPortOnSuccess flag.
// This bug was only hit when the buffer passed into HttpReceiveClientCertificate
// (1500 bytes initially) is tool small for the certificate.
// (1500 bytes initially) is too small for the certificate.
// Due to this bug in downlevel operating systems the FileCompletionNotificationModes.SkipCompletionPortOnSuccess
// flag is only used on Win8 and later.
internal static readonly bool SkipIOCPCallbackOnSuccess = ComNetOS.IsWin8orLater;

View File

@ -221,12 +221,14 @@ namespace Microsoft.AspNetCore.Server.WebListener
using (Utilities.CreateHttpServer(out address, async httpContext =>
{
var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
await sendFile.SendFileAsync(AbsoluteFilePath, 1234567, null, CancellationToken.None);
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
sendFile.SendFileAsync(AbsoluteFilePath, 1234567, null, CancellationToken.None));
completed = true;
}))
{
await Assert.ThrowsAsync<HttpRequestException>(() => SendRequestAsync(address));
Assert.False(completed);
var response = await SendRequestAsync(address);
response.EnsureSuccessStatusCode();
Assert.True(completed);
}
}
@ -238,12 +240,14 @@ namespace Microsoft.AspNetCore.Server.WebListener
using (Utilities.CreateHttpServer(out address, async httpContext =>
{
var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
await sendFile.SendFileAsync(AbsoluteFilePath, 0, 1234567, CancellationToken.None);
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
sendFile.SendFileAsync(AbsoluteFilePath, 0, 1234567, CancellationToken.None));
completed = true;
}))
{
await Assert.ThrowsAsync<HttpRequestException>(() => SendRequestAsync(address));
Assert.False(completed);
var response = await SendRequestAsync(address);
response.EnsureSuccessStatusCode();
Assert.True(completed);
}
}

View File

@ -30,14 +30,15 @@ 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();
await Assert.ThrowsAsync<FileNotFoundException>(() =>
context.Response.SendFileAsync("Missing.txt", 0, null, CancellationToken.None));
context.Dispose();
HttpResponseMessage response = await responseTask;
var response = await responseTask;
response.EnsureSuccessStatusCode();
}
}
@ -47,13 +48,13 @@ 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();
await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
context.Dispose();
HttpResponseMessage response = await responseTask;
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
IEnumerable<string> ignored;
Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
@ -68,13 +69,13 @@ 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();
await context.Response.SendFileAsync(RelativeFilePath, 0, null, CancellationToken.None);
context.Dispose();
HttpResponseMessage response = await responseTask;
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
IEnumerable<string> ignored;
Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
@ -89,13 +90,13 @@ 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();
await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
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");
@ -110,14 +111,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();
await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
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");
@ -132,13 +133,13 @@ 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();
await context.Response.SendFileAsync(AbsoluteFilePath, 0, FileLength / 2, CancellationToken.None);
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");
@ -153,14 +154,15 @@ 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();
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
() => context.Response.SendFileAsync(AbsoluteFilePath, 1234567, null, CancellationToken.None));
context.Dispose();
HttpResponseMessage response = await responseTask;
var response = await responseTask;
response.EnsureSuccessStatusCode();
}
}
@ -170,14 +172,15 @@ 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();
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
() => context.Response.SendFileAsync(AbsoluteFilePath, 0, 1234567, CancellationToken.None));
context.Dispose();
HttpResponseMessage response = await responseTask;
var response = await responseTask;
response.EnsureSuccessStatusCode();
}
}
@ -187,13 +190,13 @@ 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();
await context.Response.SendFileAsync(AbsoluteFilePath, 0, 0, CancellationToken.None);
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");
@ -212,7 +215,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();
await context.Response.SendFileAsync(emptyFilePath, 0, null, CancellationToken.None);
@ -221,7 +224,7 @@ namespace Microsoft.Net.Http.Server
context.Dispose();
File.Delete(emptyFilePath);
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");
@ -236,13 +239,13 @@ 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"] = FileLength.ToString();
await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
HttpResponseMessage response = await responseTask;
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
IEnumerable<string> contentLength;
Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
@ -258,14 +261,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.Headers["Content-lenGth"] = "10";
await context.Response.SendFileAsync(AbsoluteFilePath, 0, 10, CancellationToken.None);
context.Dispose();
HttpResponseMessage response = await responseTask;
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
IEnumerable<string> contentLength;
Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
@ -281,14 +284,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.Headers["Content-lenGth"] = "0";
await context.Response.SendFileAsync(AbsoluteFilePath, 0, 0, CancellationToken.None);
context.Dispose();
HttpResponseMessage response = await responseTask;
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
IEnumerable<string> contentLength;
Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
@ -313,7 +316,7 @@ namespace Microsoft.Net.Http.Server
await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
context.Dispose();
HttpResponseMessage response = await responseTask;
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal(FileLength * 2, (await response.Content.ReadAsByteArrayAsync()).Length);
}
@ -335,7 +338,7 @@ namespace Microsoft.Net.Http.Server
await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
context.Dispose();
HttpResponseMessage response = await responseTask;
var response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal(FileLength * 2, (await response.Content.ReadAsByteArrayAsync()).Length);
}