File: ResponseSendFileTests.cs
Web Access
Project: src\src\Servers\HttpSys\test\FunctionalTests\Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj (Microsoft.AspNetCore.Server.HttpSys.FunctionalTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Logging;
using Xunit;
 
namespace Microsoft.AspNetCore.Server.HttpSys;
 
public class ResponseSendFileTests : LoggedTest
{
    private readonly string AbsoluteFilePath;
    private readonly string RelativeFilePath;
    private readonly long FileLength;
 
    public ResponseSendFileTests()
    {
        AbsoluteFilePath = Directory.GetFiles(Directory.GetCurrentDirectory()).First();
        RelativeFilePath = Path.GetFileName(AbsoluteFilePath);
        FileLength = new FileInfo(AbsoluteFilePath).Length;
    }
 
    [ConditionalFact]
    public async Task ResponseSendFile_MissingFile_Throws()
    {
        var appThrew = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
        using (Utilities.CreateHttpServer(out var address, async httpContext =>
        {
            var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
            try
            {
                await sendFile.SendFileAsync(string.Empty, 0, null, CancellationToken.None);
                appThrew.SetResult(false);
            }
            catch (Exception)
            {
                appThrew.SetResult(true);
                throw;
            }
        }, LoggerFactory))
        {
            HttpResponseMessage response = await SendRequestAsync(address);
            Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
            Assert.True(await appThrew.Task.TimeoutAfter(TimeSpan.FromSeconds(10)));
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFile_NoHeaders_DefaultsToChunked()
    {
        string address;
        using (Utilities.CreateHttpServer(out address, httpContext =>
        {
            var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
            return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
        }, LoggerFactory))
        {
            HttpResponseMessage response = await SendRequestAsync(address);
            Assert.Equal(200, (int)response.StatusCode);
            IEnumerable<string> ignored;
            Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
            Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
            Assert.Equal(FileLength, (await response.Content.ReadAsByteArrayAsync()).Length);
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFile_RelativeFile_Success()
    {
        string address;
        using (Utilities.CreateHttpServer(out address, httpContext =>
        {
            var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
            return sendFile.SendFileAsync(RelativeFilePath, 0, null, CancellationToken.None);
        }, LoggerFactory))
        {
            HttpResponseMessage response = await SendRequestAsync(address);
            Assert.Equal(200, (int)response.StatusCode);
            IEnumerable<string> ignored;
            Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
            Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
            Assert.Equal(FileLength, (await response.Content.ReadAsByteArrayAsync()).Length);
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFile_Unspecified_Chunked()
    {
        string address;
        using (Utilities.CreateHttpServer(out address, httpContext =>
        {
            var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
            return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
        }, LoggerFactory))
        {
            var response = await SendRequestAsync(address);
            Assert.Equal(200, (int)response.StatusCode);
            IEnumerable<string> contentLength;
            Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
            Assert.True(response.Headers.TransferEncodingChunked.Value);
            Assert.Equal(FileLength, (await response.Content.ReadAsByteArrayAsync()).Length);
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFile_MultipleWrites_Chunked()
    {
        string address;
        using (Utilities.CreateHttpServer(out address, httpContext =>
        {
            var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
            sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None).Wait();
            return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
        }, LoggerFactory))
        {
            var response = await SendRequestAsync(address);
            Assert.Equal(200, (int)response.StatusCode);
            IEnumerable<string> contentLength;
            Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
            Assert.True(response.Headers.TransferEncodingChunked.Value);
            Assert.Equal(FileLength * 2, (await response.Content.ReadAsByteArrayAsync()).Length);
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFile_HalfOfFile_Chunked()
    {
        string address;
        using (Utilities.CreateHttpServer(out address, httpContext =>
        {
            var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
            return sendFile.SendFileAsync(AbsoluteFilePath, 0, FileLength / 2, CancellationToken.None);
        }, LoggerFactory))
        {
            var response = await SendRequestAsync(address);
            Assert.Equal(200, (int)response.StatusCode);
            IEnumerable<string> contentLength;
            Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
            Assert.True(response.Headers.TransferEncodingChunked.Value);
            Assert.Equal(FileLength / 2, (await response.Content.ReadAsByteArrayAsync()).Length);
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFile_OffsetOutOfRange_Throws()
    {
        var completed = false;
        string address;
        using (Utilities.CreateHttpServer(out address, async httpContext =>
        {
            var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
            await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
                sendFile.SendFileAsync(AbsoluteFilePath, 1234567, null, CancellationToken.None));
            completed = true;
        }, LoggerFactory))
        {
            var response = await SendRequestAsync(address);
            response.EnsureSuccessStatusCode();
            Assert.True(completed);
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFile_CountOutOfRange_Throws()
    {
        var completed = false;
        string address;
        using (Utilities.CreateHttpServer(out address, async httpContext =>
        {
            var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
            await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
                sendFile.SendFileAsync(AbsoluteFilePath, 0, 1234567, CancellationToken.None));
            completed = true;
        }, LoggerFactory))
        {
            var response = await SendRequestAsync(address);
            response.EnsureSuccessStatusCode();
            Assert.True(completed);
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFile_Count0_Chunked()
    {
        string address;
        using (Utilities.CreateHttpServer(out address, httpContext =>
        {
            var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
            return sendFile.SendFileAsync(AbsoluteFilePath, 0, 0, CancellationToken.None);
        }, LoggerFactory))
        {
            var response = await SendRequestAsync(address);
            Assert.Equal(200, (int)response.StatusCode);
            IEnumerable<string> contentLength;
            Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
            Assert.True(response.Headers.TransferEncodingChunked.Value);
            Assert.Empty((await response.Content.ReadAsByteArrayAsync()));
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFile_ContentLength_PassedThrough()
    {
        string address;
        using (Utilities.CreateHttpServer(out address, httpContext =>
        {
            var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
            httpContext.Response.Headers["Content-lenGth"] = FileLength.ToString(CultureInfo.InvariantCulture);
            return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
        }, LoggerFactory))
        {
            var response = await SendRequestAsync(address);
            Assert.Equal(200, (int)response.StatusCode);
            IEnumerable<string> contentLength;
            Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
            Assert.Equal(FileLength.ToString(CultureInfo.InvariantCulture), contentLength.First());
            Assert.Null(response.Headers.TransferEncodingChunked);
            Assert.Equal(FileLength, (await response.Content.ReadAsByteArrayAsync()).Length);
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFile_ContentLengthSpecific_PassedThrough()
    {
        string address;
        using (Utilities.CreateHttpServer(out address, httpContext =>
        {
            var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
            httpContext.Response.Headers["Content-lenGth"] = "10";
            return sendFile.SendFileAsync(AbsoluteFilePath, 0, 10, CancellationToken.None);
        }, LoggerFactory))
        {
            var response = await SendRequestAsync(address);
            Assert.Equal(200, (int)response.StatusCode);
            IEnumerable<string> contentLength;
            Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
            Assert.Equal("10", contentLength.First());
            Assert.Null(response.Headers.TransferEncodingChunked);
            Assert.Equal(10, (await response.Content.ReadAsByteArrayAsync()).Length);
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFile_ContentLength0_PassedThrough()
    {
        string address;
        using (Utilities.CreateHttpServer(out address, httpContext =>
        {
            var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
            httpContext.Response.Headers["Content-lenGth"] = "0";
            return sendFile.SendFileAsync(AbsoluteFilePath, 0, 0, CancellationToken.None);
        }, LoggerFactory))
        {
            var response = await SendRequestAsync(address);
            Assert.Equal(200, (int)response.StatusCode);
            IEnumerable<string> contentLength;
            Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
            Assert.Equal("0", contentLength.First());
            Assert.Null(response.Headers.TransferEncodingChunked);
            Assert.Empty((await response.Content.ReadAsByteArrayAsync()));
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFile_TriggersOnStarting()
    {
        var onStartingCalled = false;
        string address;
        using (Utilities.CreateHttpServer(out address, httpContext =>
        {
            httpContext.Response.OnStarting(state =>
            {
                onStartingCalled = true;
                Assert.Same(state, httpContext);
                return Task.FromResult(0);
            }, httpContext);
            var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
            return sendFile.SendFileAsync(AbsoluteFilePath, 0, 10, CancellationToken.None);
        }, LoggerFactory))
        {
            var response = await SendRequestAsync(address);
            Assert.Equal(200, (int)response.StatusCode);
            Assert.Equal(new Version(1, 1), response.Version);
            Assert.True(onStartingCalled);
            IEnumerable<string> ignored;
            Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
            Assert.True(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
            Assert.Equal(10, (await response.Content.ReadAsByteArrayAsync()).Length);
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFile_EmptyFileCountUnspecified_SetsChunkedAndFlushesHeaders()
    {
        var emptyFilePath = Path.Combine(Directory.GetCurrentDirectory(), "zz_" + Guid.NewGuid().ToString() + "EmptyTestFile.txt");
        var emptyFile = File.Create(emptyFilePath, 1024);
        emptyFile.Dispose();
        try
        {
            using (Utilities.CreateHttpServer(out var address, async httpContext =>
            {
                await httpContext.Response.SendFileAsync(emptyFilePath, 0, null, CancellationToken.None);
                Assert.True(httpContext.Response.HasStarted);
                await httpContext.Response.Body.WriteAsync(new byte[10], 0, 10, CancellationToken.None);
            }, LoggerFactory))
            {
                var response = await SendRequestAsync(address);
                Assert.Equal(200, (int)response.StatusCode);
                Assert.False(response.Content.Headers.TryGetValues("content-length", out var contentLength), "Content-Length");
                Assert.True(response.Headers.TransferEncodingChunked.HasValue);
                Assert.Equal(10, (await response.Content.ReadAsByteArrayAsync()).Length);
            }
        }
        finally
        {
            File.Delete(emptyFilePath);
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFile_WithActiveCancellationToken_Success()
    {
        using (Utilities.CreateHttpServer(out var address, async httpContext =>
        {
            var cts = new CancellationTokenSource();
            // First write sends headers
            await httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
            await httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
        }, LoggerFactory))
        {
            var response = await SendRequestAsync(address);
            Assert.Equal(200, (int)response.StatusCode);
            Assert.Equal(FileLength * 2, (await response.Content.ReadAsByteArrayAsync()).Length);
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFile_WithTimerCancellationToken_Success()
    {
        using (Utilities.CreateHttpServer(out var address, async httpContext =>
        {
            var cts = new CancellationTokenSource();
            cts.CancelAfter(TimeSpan.FromSeconds(10));
            // First write sends headers
            await httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
            await httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
        }, LoggerFactory))
        {
            var response = await SendRequestAsync(address);
            Assert.Equal(200, (int)response.StatusCode);
            Assert.Equal(FileLength * 2, (await response.Content.ReadAsByteArrayAsync()).Length);
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFileWriteExceptions_FirstCallWithCanceledCancellationToken_CancelsAndAborts()
    {
        var testComplete = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        using (Utilities.CreateHttpServer(out var address, httpContext =>
        {
            try
            {
                var cts = new CancellationTokenSource();
                cts.Cancel();
                // First write sends headers
                var writeTask = httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
                Assert.True(writeTask.IsCanceled);
                testComplete.SetResult();
            }
            catch (Exception ex)
            {
                testComplete.SetException(ex);
            }
 
            return Task.CompletedTask;
        }, options => options.ThrowWriteExceptions = true, LoggerFactory))
        {
            await Assert.ThrowsAsync<HttpRequestException>(() => SendRequestAsync(address));
            await testComplete.Task.DefaultTimeout();
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFile_FirstSendWithCanceledCancellationToken_CancelsAndAborts()
    {
        var testComplete = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        using (Utilities.CreateHttpServer(out var address, httpContext =>
        {
            try
            {
                var cts = new CancellationTokenSource();
                cts.Cancel();
                // First write sends headers
                var writeTask = httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
                Assert.True(writeTask.IsCanceled);
                testComplete.SetResult();
            }
            catch (Exception ex)
            {
                testComplete.SetException(ex);
            }
 
            return Task.CompletedTask;
        }, LoggerFactory))
        {
            await Assert.ThrowsAsync<HttpRequestException>(() => SendRequestAsync(address));
            await testComplete.Task.DefaultTimeout();
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFileExceptions_SecondSendWithCanceledCancellationToken_CancelsAndAborts()
    {
        var testComplete = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        using (Utilities.CreateHttpServer(out var address, async httpContext =>
        {
            try
            {
                var cts = new CancellationTokenSource();
                // First write sends headers
                await httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
                cts.Cancel();
                var writeTask = httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
                Assert.True(writeTask.IsCanceled);
                testComplete.SetResult();
            }
            catch (Exception ex)
            {
                testComplete.SetException(ex);
            }
        }, options => options.ThrowWriteExceptions = true, LoggerFactory))
        {
            await Assert.ThrowsAsync<HttpRequestException>(() => SendRequestAsync(address));
            await testComplete.Task.DefaultTimeout();
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFile_SecondSendWithCanceledCancellationToken_CancelsAndAborts()
    {
        var testComplete = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        using (Utilities.CreateHttpServer(out var address, async httpContext =>
        {
            try
            {
                var cts = new CancellationTokenSource();
                // First write sends headers
                await httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
                cts.Cancel();
                var writeTask = httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
                Assert.True(writeTask.IsCanceled);
                testComplete.SetResult();
            }
            catch (Exception ex)
            {
                testComplete.SetException(ex);
            }
        }, LoggerFactory))
        {
            await Assert.ThrowsAsync<HttpRequestException>(() => SendRequestAsync(address));
            await testComplete.Task.DefaultTimeout();
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFileExceptions_ClientDisconnectsBeforeFirstSend_SendThrows()
    {
        var requestReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var requestCancelled = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var cancellationReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var testComplete = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        using (Utilities.CreateHttpServer(out var address, async httpContext =>
        {
            httpContext.RequestAborted.Register(() => cancellationReceived.SetResult());
            requestReceived.SetResult();
            await requestCancelled.Task;
 
            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 () =>
                {
                    // It can take several tries before Send notices the disconnect.
                    for (int i = 0; i < Utilities.WriteRetryLimit; i++)
                    {
                        await httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
                    }
                });
 
                await Assert.ThrowsAsync<ObjectDisposedException>(() =>
                    httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token));
 
                testComplete.SetResult();
            }
            catch (Exception ex)
            {
                testComplete.SetException(ex);
            }
 
        }, options => options.ThrowWriteExceptions = true, LoggerFactory))
        {
            var cts = new CancellationTokenSource();
            var responseTask = SendRequestAsync(address, cts.Token);
            await requestReceived.Task.DefaultTimeout();
            // First write sends headers
            cts.Cancel();
            await Assert.ThrowsAnyAsync<OperationCanceledException>(() => responseTask);
            requestCancelled.SetResult();
 
            await testComplete.Task.DefaultTimeout();
            await cancellationReceived.Task.DefaultTimeout();
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFile_ClientDisconnectsBeforeFirstSend_SendCompletesSilently()
    {
        var requestReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var requestCancelled = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var cancellationReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var testComplete = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        using (Utilities.CreateHttpServer(out var address, async httpContext =>
        {
            httpContext.RequestAborted.Register(() => cancellationReceived.SetResult());
            requestReceived.SetResult();
            await requestCancelled.Task;
 
            try
            {
                // It can take several tries before Send notices the disconnect.
                for (int i = 0; i < Utilities.WriteRetryLimit; i++)
                {
                    await httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null);
                }
 
                testComplete.SetResult();
            }
            catch (Exception ex)
            {
                testComplete.SetException(ex);
            }
 
        }, LoggerFactory))
        {
            var cts = new CancellationTokenSource();
            var responseTask = SendRequestAsync(address, cts.Token);
            await requestReceived.Task.DefaultTimeout();
            // First write sends headers
            cts.Cancel();
            await Assert.ThrowsAnyAsync<OperationCanceledException>(() => responseTask);
            requestCancelled.SetResult();
 
            await testComplete.Task.DefaultTimeout();
            await cancellationReceived.Task.DefaultTimeout();
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFileExceptions_ClientDisconnectsBeforeSecondSend_SendThrows()
    {
        var firstSendComplete = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var clientDisconnected = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var cancellationReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var testComplete = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        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());
            // First write sends headers
            await httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
            firstSendComplete.SetResult();
            await clientDisconnected.Task;
 
            try
            {
                await Assert.ThrowsAsync<IOException>(async () =>
                {
                    // It can take several tries before Write notices the disconnect.
                    for (int i = 0; i < Utilities.WriteRetryLimit; i++)
                    {
                        await httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
                    }
                });
 
                testComplete.SetResult();
            }
            catch (Exception ex)
            {
                testComplete.SetException(ex);
            }
        }, options => options.ThrowWriteExceptions = true, LoggerFactory))
        {
            using (var client = new HttpClient())
            {
                var response = await client.GetAsync(address, HttpCompletionOption.ResponseHeadersRead);
                response.EnsureSuccessStatusCode();
                // Drain data from the connection so that SendFileAsync can complete.
                var bufferTask = response.Content.LoadIntoBufferAsync();
 
                await firstSendComplete.Task.DefaultTimeout();
 
                // Abort
                response.Dispose();
            }
            clientDisconnected.SetResult();
            await testComplete.Task.DefaultTimeout();
            await cancellationReceived.Task.DefaultTimeout();
        }
    }
 
    [ConditionalFact]
    public async Task ResponseSendFile_ClientDisconnectsBeforeSecondSend_SendCompletesSilently()
    {
        var firstSendComplete = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var clientDisconnected = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var cancellationReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var testComplete = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        using (Utilities.CreateHttpServer(out var address, async httpContext =>
        {
            httpContext.RequestAborted.Register(() => cancellationReceived.SetResult());
            // First write sends headers
            await httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null);
            firstSendComplete.SetResult();
            await clientDisconnected.Task;
 
            try
            {
                // It can take several tries before Write notices the disconnect.
                for (int i = 0; i < Utilities.WriteRetryLimit; i++)
                {
                    await httpContext.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
                }
 
                testComplete.SetResult();
            }
            catch (Exception ex)
            {
                testComplete.SetException(ex);
            }
        }, LoggerFactory))
        {
            using (var client = new HttpClient())
            {
                var response = await client.GetAsync(address, HttpCompletionOption.ResponseHeadersRead);
                response.EnsureSuccessStatusCode();
                // Drain data from the connection so that SendFileAsync can complete.
                var bufferTask = response.Content.LoadIntoBufferAsync();
 
                await firstSendComplete.Task.DefaultTimeout();
 
                // Abort
                response.Dispose();
            }
            clientDisconnected.SetResult();
            await testComplete.Task.DefaultTimeout();
            await cancellationReceived.Task.DefaultTimeout();
        }
    }
 
    private async Task<HttpResponseMessage> SendRequestAsync(string uri, CancellationToken cancellationToken = new CancellationToken())
    {
        using (HttpClient client = new HttpClient() { Timeout = Utilities.DefaultTimeout })
        {
            return await client.GetAsync(uri, cancellationToken);
        }
    }
}