Skip to content

Commit 503ef7d

Browse files
halter73mkArtakMSFT
authored andcommitted
Don't double-flush HTTP/1 Content-Length responses (#11825)
* Don't double-flush HTTP/1 Content-Length responses * PR feedback
1 parent 28d3923 commit 503ef7d

File tree

2 files changed

+77
-10
lines changed

2 files changed

+77
-10
lines changed

src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ internal class Http1OutputProducer : IHttpOutputProducer, IHttpOutputAborter, ID
5555
// and append the end terminator.
5656

5757
private bool _autoChunk;
58-
private bool _suffixSent;
58+
59+
// We rely on the TimingPipeFlusher to give us ValueTasks that can be safely awaited multiple times.
60+
private bool _writeStreamSuffixCalled;
61+
private ValueTask<FlushResult> _writeStreamSuffixValueTask;
62+
5963
private int _advancedBytesForChunk;
6064
private Memory<byte> _currentChunkMemory;
6165
private bool _currentChunkMemoryUpdated;
@@ -113,15 +117,28 @@ public ValueTask<FlushResult> WriteStreamSuffixAsync()
113117
{
114118
lock (_contextLock)
115119
{
116-
if (_suffixSent || !_autoChunk)
120+
if (_writeStreamSuffixCalled)
117121
{
118-
_suffixSent = true;
119-
return FlushAsync();
122+
// If WriteStreamSuffixAsync has already been called, no-op and return the previously returned ValueTask.
123+
return _writeStreamSuffixValueTask;
120124
}
121125

122-
_suffixSent = true;
123-
var writer = new BufferWriter<PipeWriter>(_pipeWriter);
124-
return WriteAsyncInternal(ref writer, EndChunkedResponseBytes);
126+
if (_autoChunk)
127+
{
128+
var writer = new BufferWriter<PipeWriter>(_pipeWriter);
129+
_writeStreamSuffixValueTask = WriteAsyncInternal(ref writer, EndChunkedResponseBytes);
130+
}
131+
else if (_unflushedBytes > 0)
132+
{
133+
_writeStreamSuffixValueTask = FlushAsync();
134+
}
135+
else
136+
{
137+
_writeStreamSuffixValueTask = default;
138+
}
139+
140+
_writeStreamSuffixCalled = true;
141+
return _writeStreamSuffixValueTask;
125142
}
126143
}
127144

@@ -510,7 +527,8 @@ public void Reset()
510527
// Cleared in sequential address ascending order
511528
_currentMemoryPrefixBytes = 0;
512529
_autoChunk = false;
513-
_suffixSent = false;
530+
_writeStreamSuffixCalled = false;
531+
_writeStreamSuffixValueTask = default;
514532
_currentChunkMemoryUpdated = false;
515533
_startCalled = false;
516534
}
@@ -701,7 +719,7 @@ private void AddSegment(int sizeHint = 0)
701719
[StackTraceHidden]
702720
private void ThrowIfSuffixSent()
703721
{
704-
if (_suffixSent)
722+
if (_writeStreamSuffixCalled)
705723
{
706724
throw new InvalidOperationException("Writing is not allowed after writer was completed.");
707725
}

src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseTests.cs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2903,7 +2903,7 @@ public async Task LargeWriteWithContentLengthWritingWorks()
29032903
var expectedString = new string('a', expectedLength);
29042904
await using (var server = new TestServer(async httpContext =>
29052905
{
2906-
httpContext.Response.Headers["Content-Length"] = new[] { expectedLength.ToString() };
2906+
httpContext.Response.ContentLength = expectedLength;
29072907
await httpContext.Response.WriteAsync(expectedString);
29082908
Assert.True(httpContext.Response.HasStarted);
29092909
}, testContext))
@@ -2925,6 +2925,55 @@ await connection.Receive(
29252925
}
29262926
}
29272927

2928+
[Fact]
2929+
public async Task UnflushedContentLengthResponseIsFlushedAutomatically()
2930+
{
2931+
var testContext = new TestServiceContext(LoggerFactory);
2932+
var expectedLength = 100000;
2933+
var expectedString = new string('a', expectedLength);
2934+
2935+
void WriteStringWithoutFlushing(PipeWriter writer, string content)
2936+
{
2937+
var encoder = Encoding.ASCII.GetEncoder();
2938+
var encodedLength = Encoding.ASCII.GetByteCount(expectedString);
2939+
var source = expectedString.AsSpan();
2940+
var completed = false;
2941+
2942+
while (!completed)
2943+
{
2944+
encoder.Convert(source, writer.GetSpan(), flush: source.Length == 0, out var charsUsed, out var bytesUsed, out completed);
2945+
writer.Advance(bytesUsed);
2946+
source = source.Slice(charsUsed);
2947+
}
2948+
}
2949+
2950+
await using (var server = new TestServer(httpContext =>
2951+
{
2952+
httpContext.Response.ContentLength = expectedLength;
2953+
2954+
WriteStringWithoutFlushing(httpContext.Response.BodyWriter, expectedString);
2955+
2956+
Assert.False(httpContext.Response.HasStarted);
2957+
return Task.CompletedTask;
2958+
}, testContext))
2959+
{
2960+
using (var connection = server.CreateConnection())
2961+
{
2962+
await connection.Send(
2963+
"GET / HTTP/1.1",
2964+
"Host:",
2965+
"",
2966+
"");
2967+
await connection.Receive(
2968+
"HTTP/1.1 200 OK",
2969+
$"Date: {testContext.DateHeaderValue}",
2970+
$"Content-Length: {expectedLength}",
2971+
"",
2972+
expectedString);
2973+
}
2974+
}
2975+
}
2976+
29282977
[Fact]
29292978
public async Task StartAsyncAndFlushWorks()
29302979
{

0 commit comments

Comments
 (0)