Cherry pick preview8 changes into master (#24849)
* Pass RequestAborted to SendFileAsync * Fix HttpSys tests
This commit is contained in:
parent
ffeeb034f5
commit
3b9c16ce28
|
|
@ -16,6 +16,8 @@ namespace Microsoft.AspNetCore.Http
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SendFileResponseExtensions
|
public static class SendFileResponseExtensions
|
||||||
{
|
{
|
||||||
|
private const int StreamCopyBufferSize = 64 * 1024;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sends the given file using the SendFile extension.
|
/// Sends the given file using the SendFile extension.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -110,15 +112,21 @@ namespace Microsoft.AspNetCore.Http
|
||||||
if (string.IsNullOrEmpty(file.PhysicalPath))
|
if (string.IsNullOrEmpty(file.PhysicalPath))
|
||||||
{
|
{
|
||||||
CheckRange(offset, count, file.Length);
|
CheckRange(offset, count, file.Length);
|
||||||
|
using var fileContent = file.CreateReadStream();
|
||||||
|
|
||||||
using (var fileContent = file.CreateReadStream())
|
var useRequestAborted = !cancellationToken.CanBeCanceled;
|
||||||
|
var localCancel = useRequestAborted ? response.HttpContext.RequestAborted : cancellationToken;
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
|
localCancel.ThrowIfCancellationRequested();
|
||||||
if (offset > 0)
|
if (offset > 0)
|
||||||
{
|
{
|
||||||
fileContent.Seek(offset, SeekOrigin.Begin);
|
fileContent.Seek(offset, SeekOrigin.Begin);
|
||||||
}
|
}
|
||||||
await StreamCopyOperation.CopyToAsync(fileContent, response.Body, count, cancellationToken);
|
await StreamCopyOperation.CopyToAsync(fileContent, response.Body, count, StreamCopyBufferSize, localCancel);
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) when (useRequestAborted) { }
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -126,10 +134,17 @@ namespace Microsoft.AspNetCore.Http
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task SendFileAsyncCore(HttpResponse response, string fileName, long offset, long? count, CancellationToken cancellationToken = default)
|
private static async Task SendFileAsyncCore(HttpResponse response, string fileName, long offset, long? count, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
var useRequestAborted = !cancellationToken.CanBeCanceled;
|
||||||
|
var localCancel = useRequestAborted ? response.HttpContext.RequestAborted : cancellationToken;
|
||||||
var sendFile = response.HttpContext.Features.Get<IHttpResponseBodyFeature>();
|
var sendFile = response.HttpContext.Features.Get<IHttpResponseBodyFeature>();
|
||||||
return sendFile.SendFileAsync(fileName, offset, count, cancellationToken);
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await sendFile.SendFileAsync(fileName, offset, count, localCancel);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (useRequestAborted) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CheckRange(long offset, long? count, long fileLength)
|
private static void CheckRange(long offset, long? count, long fileLength)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="testfile1kb.txt" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Microsoft.AspNetCore.Http" />
|
<Reference Include="Microsoft.AspNetCore.Http" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
|
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
@ -29,18 +30,86 @@ namespace Microsoft.AspNetCore.Http.Extensions.Tests
|
||||||
|
|
||||||
await response.SendFileAsync("bob", 1, 3, CancellationToken.None);
|
await response.SendFileAsync("bob", 1, 3, CancellationToken.None);
|
||||||
|
|
||||||
Assert.Equal("bob", fakeFeature.name);
|
Assert.Equal("bob", fakeFeature.Name);
|
||||||
Assert.Equal(1, fakeFeature.offset);
|
Assert.Equal(1, fakeFeature.Offset);
|
||||||
Assert.Equal(3, fakeFeature.length);
|
Assert.Equal(3, fakeFeature.Length);
|
||||||
Assert.Equal(CancellationToken.None, fakeFeature.token);
|
Assert.Equal(CancellationToken.None, fakeFeature.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendFile_FallsBackToBodyStream()
|
||||||
|
{
|
||||||
|
var body = new MemoryStream();
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
var response = context.Response;
|
||||||
|
response.Body = body;
|
||||||
|
|
||||||
|
await response.SendFileAsync("testfile1kb.txt", 1, 3, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(3, body.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendFile_Stream_ThrowsWhenCanceled()
|
||||||
|
{
|
||||||
|
var body = new MemoryStream();
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
var response = context.Response;
|
||||||
|
response.Body = body;
|
||||||
|
|
||||||
|
await Assert.ThrowsAnyAsync<OperationCanceledException>(
|
||||||
|
() => response.SendFileAsync("testfile1kb.txt", 1, 3, new CancellationToken(canceled: true)));
|
||||||
|
|
||||||
|
Assert.Equal(0, body.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendFile_Feature_ThrowsWhenCanceled()
|
||||||
|
{
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
var fakeFeature = new FakeResponseBodyFeature();
|
||||||
|
context.Features.Set<IHttpResponseBodyFeature>(fakeFeature);
|
||||||
|
var response = context.Response;
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<OperationCanceledException>(
|
||||||
|
() => response.SendFileAsync("testfile1kb.txt", 1, 3, new CancellationToken(canceled: true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendFile_Stream_AbortsSilentlyWhenRequestCanceled()
|
||||||
|
{
|
||||||
|
var body = new MemoryStream();
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
context.RequestAborted = new CancellationToken(canceled: true);
|
||||||
|
var response = context.Response;
|
||||||
|
response.Body = body;
|
||||||
|
|
||||||
|
await response.SendFileAsync("testfile1kb.txt", 1, 3, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(0, body.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendFile_Feature_AbortsSilentlyWhenRequestCanceled()
|
||||||
|
{
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
var fakeFeature = new FakeResponseBodyFeature();
|
||||||
|
context.Features.Set<IHttpResponseBodyFeature>(fakeFeature);
|
||||||
|
var token = new CancellationToken(canceled: true);
|
||||||
|
context.RequestAborted = token;
|
||||||
|
var response = context.Response;
|
||||||
|
|
||||||
|
await response.SendFileAsync("testfile1kb.txt", 1, 3, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(token, fakeFeature.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FakeResponseBodyFeature : IHttpResponseBodyFeature
|
private class FakeResponseBodyFeature : IHttpResponseBodyFeature
|
||||||
{
|
{
|
||||||
public string name = null;
|
public string Name { get; set; } = null;
|
||||||
public long offset = 0;
|
public long Offset { get; set; } = 0;
|
||||||
public long? length = null;
|
public long? Length { get; set; } = null;
|
||||||
public CancellationToken token;
|
public CancellationToken Token { get; set; }
|
||||||
|
|
||||||
public Stream Stream => throw new System.NotImplementedException();
|
public Stream Stream => throw new System.NotImplementedException();
|
||||||
|
|
||||||
|
|
@ -58,10 +127,12 @@ namespace Microsoft.AspNetCore.Http.Extensions.Tests
|
||||||
|
|
||||||
public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
|
public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
|
||||||
{
|
{
|
||||||
this.name = path;
|
Name = path;
|
||||||
this.offset = offset;
|
Offset = offset;
|
||||||
this.length = length;
|
Length = length;
|
||||||
this.token = cancellation;
|
Token = cancellation;
|
||||||
|
|
||||||
|
cancellation.ThrowIfCancellationRequested();
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||||
|
|
@ -5,11 +5,9 @@ using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Extensions;
|
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Http.Headers;
|
using Microsoft.AspNetCore.Http.Headers;
|
||||||
using Microsoft.AspNetCore.Internal;
|
using Microsoft.AspNetCore.Internal;
|
||||||
|
|
@ -21,8 +19,6 @@ namespace Microsoft.AspNetCore.StaticFiles
|
||||||
{
|
{
|
||||||
internal struct StaticFileContext
|
internal struct StaticFileContext
|
||||||
{
|
{
|
||||||
private const int StreamCopyBufferSize = 64 * 1024;
|
|
||||||
|
|
||||||
private readonly HttpContext _context;
|
private readonly HttpContext _context;
|
||||||
private readonly StaticFileOptions _options;
|
private readonly StaticFileOptions _options;
|
||||||
private readonly HttpRequest _request;
|
private readonly HttpRequest _request;
|
||||||
|
|
@ -345,29 +341,14 @@ namespace Microsoft.AspNetCore.StaticFiles
|
||||||
{
|
{
|
||||||
SetCompressionMode();
|
SetCompressionMode();
|
||||||
ApplyResponseHeaders(StatusCodes.Status200OK);
|
ApplyResponseHeaders(StatusCodes.Status200OK);
|
||||||
string physicalPath = _fileInfo.PhysicalPath;
|
|
||||||
var sendFile = _context.Features.Get<IHttpResponseBodyFeature>();
|
|
||||||
if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
|
|
||||||
{
|
|
||||||
// We don't need to directly cancel this, if the client disconnects it will fail silently.
|
|
||||||
await sendFile.SendFileAsync(physicalPath, 0, _length, CancellationToken.None);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var readStream = _fileInfo.CreateReadStream())
|
await _context.Response.SendFileAsync(_fileInfo, 0, _length, _context.RequestAborted);
|
||||||
{
|
|
||||||
// Larger StreamCopyBufferSize is required because in case of FileStream readStream isn't going to be buffering
|
|
||||||
await StreamCopyOperation.CopyToAsync(readStream, _response.Body, _length, StreamCopyBufferSize, _context.RequestAborted);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException ex)
|
catch (OperationCanceledException ex)
|
||||||
{
|
{
|
||||||
_logger.WriteCancelled(ex);
|
|
||||||
// Don't throw this exception, it's most likely caused by the client disconnecting.
|
// Don't throw this exception, it's most likely caused by the client disconnecting.
|
||||||
// However, if it was cancelled for any other reason we need to prevent empty responses.
|
_logger.WriteCancelled(ex);
|
||||||
_context.Abort();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -391,31 +372,16 @@ namespace Microsoft.AspNetCore.StaticFiles
|
||||||
SetCompressionMode();
|
SetCompressionMode();
|
||||||
ApplyResponseHeaders(StatusCodes.Status206PartialContent);
|
ApplyResponseHeaders(StatusCodes.Status206PartialContent);
|
||||||
|
|
||||||
string physicalPath = _fileInfo.PhysicalPath;
|
|
||||||
var sendFile = _context.Features.Get<IHttpResponseBodyFeature>();
|
|
||||||
if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
|
|
||||||
{
|
|
||||||
_logger.SendingFileRange(_response.Headers[HeaderNames.ContentRange], physicalPath);
|
|
||||||
// We don't need to directly cancel this, if the client disconnects it will fail silently.
|
|
||||||
await sendFile.SendFileAsync(physicalPath, start, length, CancellationToken.None);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var readStream = _fileInfo.CreateReadStream())
|
var logPath = !string.IsNullOrEmpty(_fileInfo.PhysicalPath) ? _fileInfo.PhysicalPath : SubPath;
|
||||||
{
|
_logger.SendingFileRange(_response.Headers[HeaderNames.ContentRange], logPath);
|
||||||
readStream.Seek(start, SeekOrigin.Begin); // TODO: What if !CanSeek?
|
await _context.Response.SendFileAsync(_fileInfo, start, length, _context.RequestAborted);
|
||||||
_logger.CopyingFileRange(_response.Headers[HeaderNames.ContentRange], SubPath);
|
|
||||||
await StreamCopyOperation.CopyToAsync(readStream, _response.Body, length, _context.RequestAborted);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException ex)
|
catch (OperationCanceledException ex)
|
||||||
{
|
{
|
||||||
_logger.WriteCancelled(ex);
|
|
||||||
// Don't throw this exception, it's most likely caused by the client disconnecting.
|
// Don't throw this exception, it's most likely caused by the client disconnecting.
|
||||||
// However, if it was cancelled for any other reason we need to prevent empty responses.
|
_logger.WriteCancelled(ex);
|
||||||
_context.Abort();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
@ -120,6 +121,36 @@ namespace Microsoft.AspNetCore.StaticFiles
|
||||||
Assert.Equal(HttpsCompressionMode.Default, httpsCompressionFeature.Mode);
|
Assert.Equal(HttpsCompressionMode.Default, httpsCompressionFeature.Mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestAborted_DoesntThrow()
|
||||||
|
{
|
||||||
|
var options = new StaticFileOptions();
|
||||||
|
var fileProvider = new TestFileProvider();
|
||||||
|
fileProvider.AddFile("/foo.txt", new TestFileInfo
|
||||||
|
{
|
||||||
|
LastModified = new DateTimeOffset(2014, 1, 2, 3, 4, 5, TimeSpan.Zero)
|
||||||
|
});
|
||||||
|
var pathString = new PathString("/test");
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
httpContext.Request.Path = new PathString("/test/foo.txt");
|
||||||
|
httpContext.RequestAborted = new CancellationToken(canceled: true);
|
||||||
|
var body = new MemoryStream();
|
||||||
|
httpContext.Response.Body = body;
|
||||||
|
var validateResult = StaticFileMiddleware.ValidatePath(httpContext, pathString, out var subPath);
|
||||||
|
var contentTypeResult = StaticFileMiddleware.LookupContentType(new FileExtensionContentTypeProvider(), options, subPath, out var contentType);
|
||||||
|
|
||||||
|
var context = new StaticFileContext(httpContext, options, NullLogger.Instance, fileProvider, contentType, subPath);
|
||||||
|
|
||||||
|
var result = context.LookupFileInfo();
|
||||||
|
Assert.True(validateResult);
|
||||||
|
Assert.True(contentTypeResult);
|
||||||
|
Assert.True(result);
|
||||||
|
|
||||||
|
await context.SendAsync();
|
||||||
|
|
||||||
|
Assert.Equal(0, body.Length);
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class TestFileProvider : IFileProvider
|
private sealed class TestFileProvider : IFileProvider
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, IFileInfo> _files = new Dictionary<string, IFileInfo>(StringComparer.Ordinal);
|
private readonly Dictionary<string, IFileInfo> _files = new Dictionary<string, IFileInfo>(StringComparer.Ordinal);
|
||||||
|
|
|
||||||
|
|
@ -487,17 +487,21 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Note Response.SendFileAsync uses RequestAborted by default. This can cause the response to be disposed
|
||||||
|
// before it throws an IOException, but there's a race depending on when the disconnect is noticed.
|
||||||
|
// Passing our own token to skip that.
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
await Assert.ThrowsAsync<IOException>(async () =>
|
await Assert.ThrowsAsync<IOException>(async () =>
|
||||||
{
|
{
|
||||||
// It can take several tries before Send notices the disconnect.
|
// It can take several tries before Send notices the disconnect.
|
||||||
for (int i = 0; i < Utilities.WriteRetryLimit; i++)
|
for (int i = 0; i < Utilities.WriteRetryLimit; i++)
|
||||||
{
|
{
|
||||||
await httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null);
|
await httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await Assert.ThrowsAsync<ObjectDisposedException>(() =>
|
await Assert.ThrowsAsync<ObjectDisposedException>(() =>
|
||||||
httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null));
|
httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token));
|
||||||
|
|
||||||
testComplete.SetResult(0);
|
testComplete.SetResult(0);
|
||||||
}
|
}
|
||||||
|
|
@ -573,9 +577,13 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
var testComplete = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
|
var testComplete = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
using (Utilities.CreateHttpServer(out var address, async httpContext =>
|
using (Utilities.CreateHttpServer(out var address, async httpContext =>
|
||||||
{
|
{
|
||||||
|
// Note Response.SendFileAsync uses RequestAborted by default. This can cause the response to be disposed
|
||||||
|
// before it throws an IOException, but there's a race depending on when the disconnect is noticed.
|
||||||
|
// Passing our own token to skip that.
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
httpContext.RequestAborted.Register(() => cancellationReceived.SetResult(0));
|
httpContext.RequestAborted.Register(() => cancellationReceived.SetResult(0));
|
||||||
// First write sends headers
|
// First write sends headers
|
||||||
await httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null);
|
await httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
|
||||||
firstSendComplete.SetResult(0);
|
firstSendComplete.SetResult(0);
|
||||||
await clientDisconnected.Task;
|
await clientDisconnected.Task;
|
||||||
|
|
||||||
|
|
@ -586,7 +594,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
// It can take several tries before Write notices the disconnect.
|
// It can take several tries before Write notices the disconnect.
|
||||||
for (int i = 0; i < Utilities.WriteRetryLimit; i++)
|
for (int i = 0; i < Utilities.WriteRetryLimit; i++)
|
||||||
{
|
{
|
||||||
await httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
|
await httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue