Don't double-flush HTTP/1 Content-Length responses (#11825)

* Don't double-flush HTTP/1 Content-Length responses

* PR feedback
This commit is contained in:
Stephen Halter 2019-07-03 10:58:34 -07:00 committed by Artak
parent 28d3923cd9
commit 503ef7d96a
2 changed files with 77 additions and 10 deletions

View File

@ -55,7 +55,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
// and append the end terminator.
private bool _autoChunk;
private bool _suffixSent;
// We rely on the TimingPipeFlusher to give us ValueTasks that can be safely awaited multiple times.
private bool _writeStreamSuffixCalled;
private ValueTask<FlushResult> _writeStreamSuffixValueTask;
private int _advancedBytesForChunk;
private Memory<byte> _currentChunkMemory;
private bool _currentChunkMemoryUpdated;
@ -113,15 +117,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
lock (_contextLock)
{
if (_suffixSent || !_autoChunk)
if (_writeStreamSuffixCalled)
{
_suffixSent = true;
return FlushAsync();
// If WriteStreamSuffixAsync has already been called, no-op and return the previously returned ValueTask.
return _writeStreamSuffixValueTask;
}
_suffixSent = true;
var writer = new BufferWriter<PipeWriter>(_pipeWriter);
return WriteAsyncInternal(ref writer, EndChunkedResponseBytes);
if (_autoChunk)
{
var writer = new BufferWriter<PipeWriter>(_pipeWriter);
_writeStreamSuffixValueTask = WriteAsyncInternal(ref writer, EndChunkedResponseBytes);
}
else if (_unflushedBytes > 0)
{
_writeStreamSuffixValueTask = FlushAsync();
}
else
{
_writeStreamSuffixValueTask = default;
}
_writeStreamSuffixCalled = true;
return _writeStreamSuffixValueTask;
}
}
@ -510,7 +527,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
// Cleared in sequential address ascending order
_currentMemoryPrefixBytes = 0;
_autoChunk = false;
_suffixSent = false;
_writeStreamSuffixCalled = false;
_writeStreamSuffixValueTask = default;
_currentChunkMemoryUpdated = false;
_startCalled = false;
}
@ -701,7 +719,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
[StackTraceHidden]
private void ThrowIfSuffixSent()
{
if (_suffixSent)
if (_writeStreamSuffixCalled)
{
throw new InvalidOperationException("Writing is not allowed after writer was completed.");
}

View File

@ -2903,7 +2903,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
var expectedString = new string('a', expectedLength);
await using (var server = new TestServer(async httpContext =>
{
httpContext.Response.Headers["Content-Length"] = new[] { expectedLength.ToString() };
httpContext.Response.ContentLength = expectedLength;
await httpContext.Response.WriteAsync(expectedString);
Assert.True(httpContext.Response.HasStarted);
}, testContext))
@ -2925,6 +2925,55 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
}
[Fact]
public async Task UnflushedContentLengthResponseIsFlushedAutomatically()
{
var testContext = new TestServiceContext(LoggerFactory);
var expectedLength = 100000;
var expectedString = new string('a', expectedLength);
void WriteStringWithoutFlushing(PipeWriter writer, string content)
{
var encoder = Encoding.ASCII.GetEncoder();
var encodedLength = Encoding.ASCII.GetByteCount(expectedString);
var source = expectedString.AsSpan();
var completed = false;
while (!completed)
{
encoder.Convert(source, writer.GetSpan(), flush: source.Length == 0, out var charsUsed, out var bytesUsed, out completed);
writer.Advance(bytesUsed);
source = source.Slice(charsUsed);
}
}
await using (var server = new TestServer(httpContext =>
{
httpContext.Response.ContentLength = expectedLength;
WriteStringWithoutFlushing(httpContext.Response.BodyWriter, expectedString);
Assert.False(httpContext.Response.HasStarted);
return Task.CompletedTask;
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {testContext.DateHeaderValue}",
$"Content-Length: {expectedLength}",
"",
expectedString);
}
}
}
[Fact]
public async Task StartAsyncAndFlushWorks()
{