Make StartAsync not throw if we haven't started the response (#8199)

This commit is contained in:
Justin Kotalik 2019-03-07 18:32:19 -08:00 committed by GitHub
parent c0c2bb3049
commit bae2f2280a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 929 additions and 103 deletions

View File

@ -484,6 +484,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public System.Threading.Tasks.ValueTask<System.IO.Pipelines.FlushResult> FlushAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public System.Memory<byte> GetMemory(int sizeHint = 0) { throw null; }
public System.Span<byte> GetSpan(int sizeHint = 0) { throw null; }
public void Reset() { }
public System.Threading.Tasks.ValueTask<System.IO.Pipelines.FlushResult> Write100ContinueAsync() { throw null; }
public System.Threading.Tasks.ValueTask<System.IO.Pipelines.FlushResult> WriteChunkAsync(System.ReadOnlySpan<byte> buffer, System.Threading.CancellationToken cancellationToken) { throw null; }
public System.Threading.Tasks.Task WriteDataAsync(System.ReadOnlySpan<byte> buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
@ -954,6 +955,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
System.Threading.Tasks.ValueTask<System.IO.Pipelines.FlushResult> FlushAsync(System.Threading.CancellationToken cancellationToken);
System.Memory<byte> GetMemory(int sizeHint = 0);
System.Span<byte> GetSpan(int sizeHint = 0);
void Reset();
System.Threading.Tasks.ValueTask<System.IO.Pipelines.FlushResult> Write100ContinueAsync();
System.Threading.Tasks.ValueTask<System.IO.Pipelines.FlushResult> WriteChunkAsync(System.ReadOnlySpan<byte> data, System.Threading.CancellationToken cancellationToken);
System.Threading.Tasks.Task WriteDataAsync(System.ReadOnlySpan<byte> data, System.Threading.CancellationToken cancellationToken);
@ -1297,6 +1299,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
public System.Span<byte> GetSpan(int sizeHint = 0) { throw null; }
void Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpOutputAborter.Abort(Microsoft.AspNetCore.Connections.ConnectionAbortedException abortReason) { }
System.Threading.Tasks.ValueTask<System.IO.Pipelines.FlushResult> Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpOutputProducer.WriteChunkAsync(System.ReadOnlySpan<byte> data, System.Threading.CancellationToken cancellationToken) { throw null; }
public void Reset() { }
public System.Threading.Tasks.ValueTask<System.IO.Pipelines.FlushResult> Write100ContinueAsync() { throw null; }
public System.Threading.Tasks.Task WriteChunkAsync(System.ReadOnlySpan<byte> span, System.Threading.CancellationToken cancellationToken) { throw null; }
public System.Threading.Tasks.Task WriteDataAsync(System.ReadOnlySpan<byte> data, System.Threading.CancellationToken cancellationToken) { throw null; }

View File

@ -3,6 +3,7 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO.Pipelines;
using System.Threading;
@ -26,6 +27,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
// "0\r\n\r\n"
private static ReadOnlySpan<byte> EndChunkedResponseBytes => new byte[] { (byte)'0', (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' };
private const int BeginChunkLengthMax = 5;
private const int EndChunkLength = 2;
private readonly string _connectionId;
private readonly ConnectionContext _connectionContext;
private readonly IKestrelTrace _log;
@ -40,21 +44,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private bool _completed;
private bool _aborted;
private long _unflushedBytes;
private bool _autoChunk;
private readonly PipeWriter _pipeWriter;
private const int MemorySizeThreshold = 1024;
private const int BeginChunkLengthMax = 5;
private const int EndChunkLength = 2;
private IMemoryOwner<byte> _fakeMemoryOwner;
// Chunked responses need to be treated uniquely when using GetMemory + Advance.
// We need to know the size of the data written to the chunk before calling Advance on the
// PipeWriter, meaning we internally track how far we have advanced through a current chunk (_advancedBytesForChunk).
// Once write or flush is called, we modify the _currentChunkMemory to prepend the size of data written
// and append the end terminator.
private bool _autoChunk;
private int _advancedBytesForChunk;
private Memory<byte> _currentChunkMemory;
private bool _currentChunkMemoryUpdated;
private IMemoryOwner<byte> _fakeMemoryOwner;
// Fields needed to store writes before calling either startAsync or Write/FlushAsync
// These should be cleared by the end of the request
private List<CompletedBuffer> _completedSegments;
private Memory<byte> _currentSegment;
private IMemoryOwner<byte> _currentSegmentOwner;
private int _position;
private bool _startCalled;
public Http1OutputProducer(
PipeWriter pipeWriter,
@ -158,6 +169,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
return GetFakeMemory(sizeHint);
}
else if (!_startCalled)
{
return LeasedMemory(sizeHint);
}
else if (_autoChunk)
{
return GetChunkedMemory(sizeHint);
@ -177,6 +192,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
return GetFakeMemory(sizeHint).Span;
}
else if (!_startCalled)
{
return LeasedMemory(sizeHint).Span;
}
else if (_autoChunk)
{
return GetChunkedMemory(sizeHint).Span;
@ -197,16 +216,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
return;
}
if (_autoChunk)
if (!_startCalled)
{
if (bytes < 0)
if (bytes >= 0)
{
throw new ArgumentOutOfRangeException(nameof(bytes));
}
if (_currentSegment.Length - bytes < _position)
{
throw new ArgumentOutOfRangeException("Can't advance past buffer size.");
}
if (bytes + _advancedBytesForChunk > _currentChunkMemory.Length - BeginChunkLengthMax - EndChunkLength)
_position += bytes;
}
}
else if (_autoChunk)
{
if (_advancedBytesForChunk > _currentChunkMemory.Length - BeginChunkLengthMax - EndChunkLength - bytes)
{
throw new InvalidOperationException("Can't advance past buffer size.");
throw new ArgumentOutOfRangeException("Can't advance past buffer size.");
}
_advancedBytesForChunk += bytes;
}
@ -238,6 +264,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
var writer = new BufferWriter<PipeWriter>(_pipeWriter);
CommitChunkInternal(ref writer, buffer);
_unflushedBytes += writer.BytesCommitted;
}
}
@ -260,7 +287,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
writer.Commit();
_unflushedBytes += writer.BytesCommitted;
}
public void WriteResponseHeaders(int statusCode, string reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk)
@ -288,8 +314,52 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
writer.Commit();
_unflushedBytes += writer.BytesCommitted;
_autoChunk = autoChunk;
WriteDataWrittenBeforeHeaders(ref writer);
_unflushedBytes += writer.BytesCommitted;
_startCalled = true;
}
private void WriteDataWrittenBeforeHeaders(ref BufferWriter<PipeWriter> writer)
{
if (_completedSegments != null)
{
foreach (var segment in _completedSegments)
{
if (_autoChunk)
{
CommitChunkInternal(ref writer, segment.Span);
}
else
{
writer.Write(segment.Span);
writer.Commit();
}
segment.Return();
}
_completedSegments.Clear();
}
if (!_currentSegment.IsEmpty)
{
var segment = _currentSegment.Slice(0, _position);
if (_autoChunk)
{
CommitChunkInternal(ref writer, segment.Span);
}
else
{
writer.Write(segment.Span);
writer.Commit();
}
_position = 0;
DisposeCurrentSegment();
}
}
public void Dispose()
@ -302,10 +372,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_fakeMemoryOwner = null;
}
// Call dispose on any memory that wasn't written.
if (_completedSegments != null)
{
foreach (var segment in _completedSegments)
{
segment.Return();
}
}
DisposeCurrentSegment();
CompletePipe();
}
}
private void DisposeCurrentSegment()
{
_currentSegmentOwner?.Dispose();
_currentSegmentOwner = null;
_currentSegment = default;
}
private void CompletePipe()
{
if (!_pipeWriterCompleted)
@ -382,10 +470,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
CommitChunkInternal(ref writer, buffer);
_unflushedBytes += writer.BytesCommitted;
return FlushAsync(cancellationToken);
}
}
public void Reset()
{
Debug.Assert(_currentSegmentOwner == null);
Debug.Assert(_completedSegments == null || _completedSegments.Count == 0);
_autoChunk = false;
_startCalled = false;
_currentChunkMemoryUpdated = false;
}
private ValueTask<FlushResult> WriteAsync(
ReadOnlySpan<byte> buffer,
CancellationToken cancellationToken = default)
@ -454,7 +553,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
var memoryMaxLength = _currentChunkMemory.Length - BeginChunkLengthMax - EndChunkLength;
if (_advancedBytesForChunk >= memoryMaxLength - Math.Min(MemorySizeThreshold, sizeHint))
if (_advancedBytesForChunk >= memoryMaxLength - sizeHint && _advancedBytesForChunk > 0)
{
// Chunk is completely written, commit it to the pipe so GetMemory will return a new chunk of memory.
var writer = new BufferWriter<PipeWriter>(_pipeWriter);
@ -506,5 +605,91 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
return _fakeMemoryOwner.Memory;
}
private Memory<byte> LeasedMemory(int sizeHint)
{
EnsureCapacity(sizeHint);
return _currentSegment.Slice(_position);
}
private void EnsureCapacity(int sizeHint)
{
// Only subtracts _position from the current segment length if it's non-null.
// If _currentSegment is null, it returns 0.
var remainingSize = _currentSegment.Length - _position;
// If the sizeHint is 0, any capacity will do
// Otherwise, the buffer must have enough space for the entire size hint, or we need to add a segment.
if ((sizeHint == 0 && remainingSize > 0) || (sizeHint > 0 && remainingSize >= sizeHint))
{
// We have capacity in the current segment
return;
}
AddSegment(sizeHint);
}
private void AddSegment(int sizeHint = 0)
{
if (_currentSegment.Length != 0)
{
// We're adding a segment to the list
if (_completedSegments == null)
{
_completedSegments = new List<CompletedBuffer>();
}
// Position might be less than the segment length if there wasn't enough space to satisfy the sizeHint when
// GetMemory was called. In that case we'll take the current segment and call it "completed", but need to
// ignore any empty space in it.
_completedSegments.Add(new CompletedBuffer(_currentSegmentOwner, _currentSegment, _position));
}
if (sizeHint <= _memoryPool.MaxBufferSize)
{
// Get a new buffer using the minimum segment size, unless the size hint is larger than a single segment.
// Also, the size cannot be larger than the MaxBufferSize of the MemoryPool
var owner = _memoryPool.Rent(Math.Min(sizeHint, _memoryPool.MaxBufferSize));
_currentSegment = owner.Memory;
_currentSegmentOwner = owner;
}
else
{
_currentSegment = new byte[sizeHint];
_currentSegmentOwner = null;
}
_position = 0;
}
/// <summary>
/// Holds a byte[] from the pool and a size value. Basically a Memory but guaranteed to be backed by an ArrayPool byte[], so that we know we can return it.
/// </summary>
private readonly struct CompletedBuffer
{
private readonly IMemoryOwner<byte> _memoryOwner;
public Memory<byte> Buffer { get; }
public int Length { get; }
public ReadOnlySpan<byte> Span => Buffer.Span.Slice(0, Length);
public CompletedBuffer(IMemoryOwner<byte> owner, Memory<byte> buffer, int length)
{
_memoryOwner = owner;
Buffer = buffer;
Length = length;
}
public void Return()
{
if (_memoryOwner != null)
{
_memoryOwner.Dispose();
}
}
}
}
}

View File

@ -45,7 +45,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
// Keep-alive is default for HTTP/1.1 and HTTP/2; parsing and errors will change its value
// volatile, see: https://msdn.microsoft.com/en-us/library/x13ttww7.aspx
protected volatile bool _keepAlive = true;
private bool _canWriteResponseBody;
// _canWriteResponseBody is set in CreateResponseHeaders.
// If we are writing with GetMemory/Advance before calling StartAsync, assume we can write and throw away contents if we can't.
private bool _canWriteResponseBody = true;
private bool _hasAdvanced;
private bool _isLeasedMemoryInvalid = true;
private bool _autoChunk;
protected Exception _applicationException;
private BadHttpRequestException _requestRejectedException;
@ -351,6 +355,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
RequestHeaders = HttpRequestHeaders;
ResponseHeaders = HttpResponseHeaders;
_isLeasedMemoryInvalid = true;
_hasAdvanced = false;
_canWriteResponseBody = true;
if (_scheme == null)
{
var tlsFeature = ConnectionFeatures?[typeof(ITlsConnectionFeature)];
@ -380,6 +388,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
Output?.Reset();
_requestHeadersParsed = 0;
_responseBytesWritten = 0;
@ -921,6 +931,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
return;
}
_isLeasedMemoryInvalid = true;
_requestProcessingStatus = RequestProcessingStatus.HeadersCommitted;
var responseHeaders = CreateResponseHeaders(appCompleted);
@ -1066,7 +1078,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
_keepAlive = false;
}
else if (appCompleted || !_canWriteResponseBody)
else if ((appCompleted || !_canWriteResponseBody) && !_hasAdvanced) // Avoid setting contentLength of 0 if we wrote data before calling CreateResponseHeaders
{
// Don't set the Content-Length header automatically for HEAD requests, 204 responses, or 304 responses.
if (CanAutoSetContentLengthZeroResponseHeader())
@ -1268,6 +1280,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public void Advance(int bytes)
{
if (bytes < 0)
{
throw new ArgumentOutOfRangeException(nameof(bytes));
}
else if (bytes > 0)
{
_hasAdvanced = true;
}
if (_isLeasedMemoryInvalid)
{
throw new InvalidOperationException("Invalid ordering of calling StartAsync and Advance. " +
"Call StartAsync before calling GetMemory/GetSpan and Advance.");
}
if (_canWriteResponseBody)
{
VerifyAndUpdateWrite(bytes);
@ -1276,7 +1303,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
else
{
HandleNonBodyResponseWrite();
// For HEAD requests, we still use the number of bytes written for logging
// how many bytes were written.
VerifyAndUpdateWrite(bytes);
@ -1285,27 +1311,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public Memory<byte> GetMemory(int sizeHint = 0)
{
ThrowIfResponseNotStarted();
_isLeasedMemoryInvalid = false;
return Output.GetMemory(sizeHint);
}
public Span<byte> GetSpan(int sizeHint = 0)
{
ThrowIfResponseNotStarted();
_isLeasedMemoryInvalid = false;
return Output.GetSpan(sizeHint);
}
[StackTraceHidden]
private void ThrowIfResponseNotStarted()
{
if (!HasResponseStarted)
{
throw new InvalidOperationException(CoreStrings.StartAsyncBeforeGetMemory);
}
}
public ValueTask<FlushResult> FlushPipeAsync(CancellationToken cancellationToken)
{
if (!HasResponseStarted)
@ -1338,6 +1353,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
ApplicationAbort();
}
}
Output.Complete();
}

View File

@ -25,5 +25,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
void Complete();
ValueTask<FlushResult> FirstWriteAsync(int statusCode, string reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk, ReadOnlySpan<byte> data, CancellationToken cancellationToken);
ValueTask<FlushResult> FirstWriteChunkedAsync(int statusCode, string reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk, ReadOnlySpan<byte> data, CancellationToken cancellationToken);
void Reset();
}
}

View File

@ -279,6 +279,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
// This will noop for now. See: https://github.com/aspnet/AspNetCore/issues/7370
}
public void Reset()
{
}
private async ValueTask<FlushResult> ProcessDataWrites()
{
FlushResult flushResult = default;

View File

@ -421,6 +421,52 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
}
[Fact]
public async Task ChunksWithGetMemoryAfterStartAsyncBeforeFirstFlushStillFlushes()
{
var testContext = new TestServiceContext(LoggerFactory);
using (var server = new TestServer(async httpContext =>
{
var response = httpContext.Response;
await response.StartAsync();
var memory = response.BodyWriter.GetMemory();
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("Hello ");
fisrtPartOfResponse.CopyTo(memory);
response.BodyWriter.Advance(6);
memory = response.BodyWriter.GetMemory();
var secondPartOfResponse = Encoding.ASCII.GetBytes("World!");
secondPartOfResponse.CopyTo(memory);
response.BodyWriter.Advance(6);
await response.BodyWriter.FlushAsync();
}, 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}",
"Transfer-Encoding: chunked",
"",
"c",
"Hello World!",
"0",
"",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task ChunksWithGetMemoryBeforeFirstFlushStillFlushes()
{
@ -429,7 +475,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
using (var server = new TestServer(async httpContext =>
{
var response = httpContext.Response;
await response.StartAsync();
var memory = response.BodyWriter.GetMemory();
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("Hello ");
fisrtPartOfResponse.CopyTo(memory);
@ -476,7 +522,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
using (var server = new TestServer(async httpContext =>
{
var response = httpContext.Response;
await response.StartAsync();
var memory = response.BodyWriter.GetMemory();
length.Value = memory.Length;
@ -524,7 +569,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
[Fact]
public async Task ChunksWithGetMemoryWithInitialFlushWorks()
public async Task ChunksWithGetMemoryAndStartAsyncWithInitialFlushWorks()
{
var length = new IntAsRef();
var semaphore = new SemaphoreSlim(initialCount: 0);
@ -581,6 +626,65 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
}
[Fact]
public async Task ChunksWithGetMemoryBeforeFlushEdgeCase()
{
var length = 0;
var semaphore = new SemaphoreSlim(initialCount: 0);
var testContext = new TestServiceContext(LoggerFactory);
using (var server = new TestServer(async httpContext =>
{
var response = httpContext.Response;
await response.StartAsync();
var memory = response.BodyWriter.GetMemory();
length = memory.Length - 1;
semaphore.Release();
var fisrtPartOfResponse = Encoding.ASCII.GetBytes(new string('a', length));
fisrtPartOfResponse.CopyTo(memory);
response.BodyWriter.Advance(length);
var secondMemory = response.BodyWriter.GetMemory(6);
var secondPartOfResponse = Encoding.ASCII.GetBytes("World!");
secondPartOfResponse.CopyTo(secondMemory);
response.BodyWriter.Advance(6);
await response.BodyWriter.FlushAsync();
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host: ",
"",
"");
// Wait for length to be set
await semaphore.WaitAsync();
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {testContext.DateHeaderValue}",
"Transfer-Encoding: chunked",
"",
length.ToString("x"),
new string('a', length),
"6",
"World!",
"0",
"",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task ChunkGetMemoryMultipleAdvance()
{
@ -633,6 +737,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
using (var server = new TestServer(async httpContext =>
{
var response = httpContext.Response;
await response.StartAsync();
// To avoid using span in an async method
void NonAsyncMethod()
@ -647,8 +752,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
response.BodyWriter.Advance(6);
}
await response.StartAsync();
NonAsyncMethod();
}, testContext))
{
@ -687,6 +790,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
await response.StartAsync();
var memory = response.BodyWriter.GetMemory(4096);
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("Hello ");
fisrtPartOfResponse.CopyTo(memory);
response.BodyWriter.Advance(6);
@ -717,6 +821,48 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
}
[Fact]
public async Task ChunkGetMemoryAndWriteWithoutStart()
{
var testContext = new TestServiceContext(LoggerFactory);
using (var server = new TestServer(async httpContext =>
{
var response = httpContext.Response;
var memory = response.BodyWriter.GetMemory(4096);
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("Hello ");
fisrtPartOfResponse.CopyTo(memory);
response.BodyWriter.Advance(6);
await response.WriteAsync("World!");
}, 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}",
"Transfer-Encoding: chunked",
"",
"6",
"Hello ",
"6",
"World!",
"0",
"",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task GetMemoryWithSizeHint()
{
@ -758,10 +904,47 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
}
[Theory]
[InlineData(15)]
[InlineData(255)]
public async Task ChunkGetMemoryWithSmallerSizesWork(int writeSize)
[Fact]
public async Task GetMemoryWithSizeHintWithoutStartAsync()
{
var testContext = new TestServiceContext(LoggerFactory);
using (var server = new TestServer(async httpContext =>
{
var response = httpContext.Response;
var memory = response.BodyWriter.GetMemory(0);
Assert.Equal(4096, memory.Length);
memory = response.BodyWriter.GetMemory(1000000);
Assert.Equal(1000000, memory.Length);
await 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: 0",
"",
"");
}
await server.StopAsync();
}
}
[Theory]
[InlineData(15)]
[InlineData(255)]
public async Task ChunkGetMemoryWithoutStartWithSmallerSizesWork(int writeSize)
{
var testContext = new TestServiceContext(LoggerFactory);
@ -769,12 +952,53 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{
var response = httpContext.Response;
await response.StartAsync();
var memory = response.BodyWriter.GetMemory(4096);
var fisrtPartOfResponse = Encoding.ASCII.GetBytes(new string('a', writeSize));
fisrtPartOfResponse.CopyTo(memory);
response.BodyWriter.Advance(writeSize);
await response.BodyWriter.FlushAsync();
}, 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}",
"Transfer-Encoding: chunked",
"",
writeSize.ToString("X").ToLower(),
new string('a', writeSize),
"0",
"",
"");
}
await server.StopAsync();
}
}
[Theory]
[InlineData(15)]
[InlineData(255)]
public async Task ChunkGetMemoryWithStartWithSmallerSizesWork(int writeSize)
{
var testContext = new TestServiceContext(LoggerFactory);
using (var server = new TestServer(async httpContext =>
{
var response = httpContext.Response;
var memory = response.BodyWriter.GetMemory(4096);
var fisrtPartOfResponse = Encoding.ASCII.GetBytes(new string('a', writeSize));
fisrtPartOfResponse.CopyTo(memory);
response.BodyWriter.Advance(writeSize);
await response.BodyWriter.FlushAsync();
}, testContext))
{
using (var connection = server.CreateConnection())
@ -806,7 +1030,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
using (var server = new TestServer(async httpContext =>
{
var response = httpContext.Response;
await response.StartAsync();
var memory = response.BodyWriter.GetMemory(4096);
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("hello,");
fisrtPartOfResponse.CopyTo(memory);

View File

@ -2625,6 +2625,52 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Fact]
public async Task GetMemoryAdvance_Works()
{
var headers = new[]
{
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
};
await InitializeConnectionAsync(httpContext =>
{
var response = httpContext.Response;
var memory = response.BodyWriter.GetMemory();
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("hello,");
fisrtPartOfResponse.CopyTo(memory);
response.BodyWriter.Advance(6);
memory = response.BodyWriter.GetMemory();
var secondPartOfResponse = Encoding.ASCII.GetBytes(" world");
secondPartOfResponse.CopyTo(memory);
response.BodyWriter.Advance(6);
return Task.CompletedTask;
});
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
var dataFrame = await ExpectAsync(Http2FrameType.DATA,
withLength: 12,
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
withStreamId: 1);
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
_hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this);
Assert.Equal(2, _decodedHeaders.Count);
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
Assert.True(_helloWorldBytes.AsSpan().SequenceEqual(dataFrame.PayloadSequence.ToArray()));
}
[Fact]
public async Task GetMemoryAdvance_WithStartAsync_Works()
{
var headers = new[]
{
@ -2682,7 +2728,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
var response = httpContext.Response;
await response.StartAsync();
var memory = response.BodyWriter.GetMemory();
Assert.Equal(4096, memory.Length);
var fisrtPartOfResponse = Encoding.ASCII.GetBytes(new string('a', memory.Length));
@ -2884,7 +2929,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
var response = httpContext.Response;
await response.StartAsync();
var memory = response.BodyWriter.GetMemory(4096);
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("hello,");
@ -2960,6 +3004,49 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
}
[Fact]
public async Task WriteAsync_GetMemoryWithSizeHintAlwaysReturnsSameSizeStartAsync()
{
var headers = new[]
{
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
};
await InitializeConnectionAsync(async httpContext =>
{
var response = httpContext.Response;
var memory = response.BodyWriter.GetMemory(0);
Assert.Equal(4096, memory.Length);
memory = response.BodyWriter.GetMemory(4096);
Assert.Equal(4096, memory.Length);
await Task.CompletedTask;
});
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 55,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
var dataFrame = await ExpectAsync(Http2FrameType.DATA,
withLength: 0,
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
withStreamId: 1);
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
_hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this);
Assert.Equal(3, _decodedHeaders.Count);
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
}
[Fact]
public async Task WriteAsync_BothPipeAndStreamWorks()
{
@ -3037,7 +3124,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
var response = httpContext.Response;
response.ContentLength = 12;
await response.StartAsync();
await Task.CompletedTask;
void NonAsyncMethod()
{
@ -3084,11 +3171,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
};
await InitializeConnectionAsync(async httpContext =>
await InitializeConnectionAsync(httpContext =>
{
var response = httpContext.Response;
response.ContentLength = 12;
await response.StartAsync();
var memory = response.BodyWriter.GetMemory(4096);
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("Hello ");
@ -3098,6 +3184,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var secondPartOfResponse = Encoding.ASCII.GetBytes("World!");
secondPartOfResponse.CopyTo(memory.Slice(6));
response.BodyWriter.Advance(6);
return Task.CompletedTask;
});
await StartStreamAsync(1, headers, endStream: true);
@ -3174,8 +3261,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
var response = httpContext.Response;
response.ContentLength = 54;
await response.StartAsync();
var memory = response.BodyWriter.GetMemory(4096);
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("hello,");
fisrtPartOfResponse.CopyTo(memory);
response.BodyWriter.Advance(6);

View File

@ -3239,18 +3239,49 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
}
[Fact]
public async Task AdvanceNegativeValueThrowsArgumentOutOfRangeExceptionWithStart()
{
var testContext = new TestServiceContext(LoggerFactory);
using (var server = new TestServer(httpContext =>
{
var response = httpContext.Response;
Assert.Throws<ArgumentOutOfRangeException>(() => response.BodyWriter.Advance(-1));
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: 0",
"",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task AdvanceWithTooLargeOfAValueThrowInvalidOperationException()
{
var testContext = new TestServiceContext(LoggerFactory);
using (var server = new TestServer(async httpContext =>
using (var server = new TestServer(httpContext =>
{
var response = httpContext.Response;
await response.StartAsync();
Assert.Throws<InvalidOperationException>(() => response.BodyWriter.Advance(1));
return Task.CompletedTask;
}, testContext))
{
using (var connection = server.CreateConnection())
@ -3275,60 +3306,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
[Fact]
public async Task GetMemoryBeforeStartAsyncThrows()
public async Task ContentLengthWithoutStartAsyncWithGetSpanWorks()
{
var testContext = new TestServiceContext(LoggerFactory);
using (var server = new TestServer(httpContext =>
{
Assert.Throws<InvalidOperationException>(() => httpContext.Response.BodyWriter.GetMemory());
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: 0",
"",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task ContentLengthWithGetSpanWorks()
{
var testContext = new TestServiceContext(LoggerFactory);
using (var server = new TestServer(async httpContext =>
{
var response = httpContext.Response;
response.ContentLength = 12;
await response.StartAsync();
void NonAsyncMethod()
{
var span = response.BodyWriter.GetSpan(4096);
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("Hello ");
fisrtPartOfResponse.CopyTo(span);
response.BodyWriter.Advance(6);
var secondPartOfResponse = Encoding.ASCII.GetBytes("World!");
secondPartOfResponse.CopyTo(span.Slice(6));
response.BodyWriter.Advance(6);
}
NonAsyncMethod();
var span = response.BodyWriter.GetSpan(4096);
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("Hello ");
fisrtPartOfResponse.CopyTo(span);
response.BodyWriter.Advance(6);
var secondPartOfResponse = Encoding.ASCII.GetBytes("World!");
secondPartOfResponse.CopyTo(span.Slice(6));
response.BodyWriter.Advance(6);
return Task.CompletedTask;
}, testContext))
{
using (var connection = server.CreateConnection())
@ -3359,6 +3354,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{
var response = httpContext.Response;
response.ContentLength = 12;
await response.StartAsync();
var memory = response.BodyWriter.GetMemory(4096);
@ -3461,7 +3457,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
[Fact]
public async Task ResponseBodyPipeCompleteWithoutExceptionDoesNotThrow()
public async Task ResponseBodyWriterCompleteWithoutExceptionDoesNotThrow()
{
using (var server = new TestServer(async httpContext =>
{
@ -3488,7 +3484,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
[Fact]
public async Task ResponseBodyPipeCompleteWithoutExceptionWritesDoNotThrow()
public async Task ResponseBodyWriterCompleteWithoutExceptionWritesDoNotThrow()
{
using (var server = new TestServer(async httpContext =>
{
@ -3516,6 +3512,217 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
}
[Fact]
public async Task ResponseAdvanceStateIsResetWithMultipleReqeusts()
{
var secondRequest = false;
using (var server = new TestServer(async httpContext =>
{
if (secondRequest)
{
return;
}
var memory = httpContext.Response.BodyWriter.GetMemory();
Encoding.ASCII.GetBytes("a").CopyTo(memory);
httpContext.Response.BodyWriter.Advance(1);
await httpContext.Response.BodyWriter.FlushAsync();
secondRequest = true;
}, new TestServiceContext(LoggerFactory)))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Transfer-Encoding: chunked",
"",
"1",
"a",
"0",
"",
"");
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Content-Length: 0",
"",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task ResponseStartCalledAndAutoChunkStateIsResetWithMultipleReqeusts()
{
using (var server = new TestServer(async httpContext =>
{
var memory = httpContext.Response.BodyWriter.GetMemory();
Encoding.ASCII.GetBytes("a").CopyTo(memory);
httpContext.Response.BodyWriter.Advance(1);
await httpContext.Response.BodyWriter.FlushAsync();
}, new TestServiceContext(LoggerFactory)))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Transfer-Encoding: chunked",
"",
"1",
"a",
"0",
"",
"");
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Transfer-Encoding: chunked",
"",
"1",
"a",
"0",
"",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task ResponseStartCalledStateIsResetWithMultipleReqeusts()
{
var flip = false;
using (var server = new TestServer(async httpContext =>
{
if (flip)
{
httpContext.Response.ContentLength = 1;
var memory = httpContext.Response.BodyWriter.GetMemory();
Encoding.ASCII.GetBytes("a").CopyTo(memory);
httpContext.Response.BodyWriter.Advance(1);
await httpContext.Response.BodyWriter.FlushAsync();
}
else
{
var memory = httpContext.Response.BodyWriter.GetMemory();
Encoding.ASCII.GetBytes("a").CopyTo(memory);
httpContext.Response.BodyWriter.Advance(1);
await httpContext.Response.BodyWriter.FlushAsync();
}
flip = !flip;
}, new TestServiceContext(LoggerFactory)))
{
using (var connection = server.CreateConnection())
{
for (var i = 0; i < 3; i++)
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Transfer-Encoding: chunked",
"",
"1",
"a",
"0",
"",
"");
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Content-Length: 1",
"",
"a");
}
}
await server.StopAsync();
}
}
[Fact]
public async Task ResponseIsLeasedMemoryInvalidStateIsResetWithMultipleReqeusts()
{
var secondRequest = false;
using (var server = new TestServer(httpContext =>
{
if (secondRequest)
{
Assert.Throws<InvalidOperationException>(() => httpContext.Response.BodyWriter.Advance(1));
return Task.CompletedTask;
}
var memory = httpContext.Response.BodyWriter.GetMemory();
return Task.CompletedTask;
}, new TestServiceContext(LoggerFactory)))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Content-Length: 0",
"",
"");
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Content-Length: 0",
"",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task ResponsePipeWriterCompleteWithException()
{
@ -3579,12 +3786,111 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
}
[Fact]
public async Task ResponseCompleteGetMemoryReturnsRentedMemoryWithoutStartAsync()
{
using (var server = new TestServer(async httpContext =>
{
httpContext.Response.BodyWriter.Complete();
var memory = httpContext.Response.BodyWriter.GetMemory(); // Shouldn't throw
Assert.Equal(4096, memory.Length);
await Task.CompletedTask;
}, new TestServiceContext(LoggerFactory)))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Content-Length: 0",
"",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task ResponseGetMemoryAndStartAsyncMemoryReturnsNewMemory()
{
using (var server = new TestServer(async httpContext =>
{
var memory = httpContext.Response.BodyWriter.GetMemory();
Assert.Equal(4096, memory.Length);
await httpContext.Response.StartAsync();
// Original memory is disposed, don't compare against it.
memory = httpContext.Response.BodyWriter.GetMemory();
Assert.NotEqual(4096, memory.Length);
}, new TestServiceContext(LoggerFactory)))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Transfer-Encoding: chunked",
"",
"0",
"",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task ResponseGetMemoryAndStartAsyncAdvanceThrows()
{
using (var server = new TestServer(async httpContext =>
{
var memory = httpContext.Response.BodyWriter.GetMemory();
await httpContext.Response.StartAsync();
Assert.Throws<InvalidOperationException>(() => httpContext.Response.BodyWriter.Advance(1));
}, new TestServiceContext(LoggerFactory)))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Transfer-Encoding: chunked",
"",
"0",
"",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task ResponseCompleteGetMemoryAdvanceInLoopDoesNotThrow()
{
using (var server = new TestServer(async httpContext =>
{
await httpContext.Response.StartAsync();
httpContext.Response.BodyWriter.Complete();
for (var i = 0; i < 5; i++)
@ -3653,12 +3959,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{
var memoryStream = new MemoryStream();
httpContext.Response.Body = memoryStream;
var bodyPipe1 = httpContext.Response.BodyWriter;
var BodyWriter1 = httpContext.Response.BodyWriter;
httpContext.Response.Body = memoryStream;
var bodyPipe2 = httpContext.Response.BodyWriter;
var BodyWriter2 = httpContext.Response.BodyWriter;
Assert.NotEqual(bodyPipe1, bodyPipe2);
Assert.NotEqual(BodyWriter1, BodyWriter2);
await Task.CompletedTask;
}, new TestServiceContext(LoggerFactory)))
{
@ -3715,7 +4021,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
[Fact]
public async Task ResponseSetPipeAndBodyPipeIsWrapped()
public async Task ResponseSetPipeAndBodyWriterIsWrapped()
{
using (var server = new TestServer(async httpContext =>
{
@ -3746,7 +4052,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
[Fact]
public async Task ResponseWriteToBodyPipeAndStreamAllBlocksDisposed()
public async Task ResponseWriteToBodyWriterAndStreamAllBlocksDisposed()
{
using (var server = new TestServer(async httpContext =>
{