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:
parent
28d3923cd9
commit
503ef7d96a
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue