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.
|
// and append the end terminator.
|
||||||
|
|
||||||
private bool _autoChunk;
|
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 int _advancedBytesForChunk;
|
||||||
private Memory<byte> _currentChunkMemory;
|
private Memory<byte> _currentChunkMemory;
|
||||||
private bool _currentChunkMemoryUpdated;
|
private bool _currentChunkMemoryUpdated;
|
||||||
|
|
@ -113,15 +117,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
lock (_contextLock)
|
lock (_contextLock)
|
||||||
{
|
{
|
||||||
if (_suffixSent || !_autoChunk)
|
if (_writeStreamSuffixCalled)
|
||||||
{
|
{
|
||||||
_suffixSent = true;
|
// If WriteStreamSuffixAsync has already been called, no-op and return the previously returned ValueTask.
|
||||||
return FlushAsync();
|
return _writeStreamSuffixValueTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
_suffixSent = true;
|
if (_autoChunk)
|
||||||
var writer = new BufferWriter<PipeWriter>(_pipeWriter);
|
{
|
||||||
return WriteAsyncInternal(ref writer, EndChunkedResponseBytes);
|
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
|
// Cleared in sequential address ascending order
|
||||||
_currentMemoryPrefixBytes = 0;
|
_currentMemoryPrefixBytes = 0;
|
||||||
_autoChunk = false;
|
_autoChunk = false;
|
||||||
_suffixSent = false;
|
_writeStreamSuffixCalled = false;
|
||||||
|
_writeStreamSuffixValueTask = default;
|
||||||
_currentChunkMemoryUpdated = false;
|
_currentChunkMemoryUpdated = false;
|
||||||
_startCalled = false;
|
_startCalled = false;
|
||||||
}
|
}
|
||||||
|
|
@ -701,7 +719,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
[StackTraceHidden]
|
[StackTraceHidden]
|
||||||
private void ThrowIfSuffixSent()
|
private void ThrowIfSuffixSent()
|
||||||
{
|
{
|
||||||
if (_suffixSent)
|
if (_writeStreamSuffixCalled)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Writing is not allowed after writer was completed.");
|
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);
|
var expectedString = new string('a', expectedLength);
|
||||||
await using (var server = new TestServer(async httpContext =>
|
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);
|
await httpContext.Response.WriteAsync(expectedString);
|
||||||
Assert.True(httpContext.Response.HasStarted);
|
Assert.True(httpContext.Response.HasStarted);
|
||||||
}, testContext))
|
}, 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]
|
[Fact]
|
||||||
public async Task StartAsyncAndFlushWorks()
|
public async Task StartAsyncAndFlushWorks()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue