Expose HttpResponse PipeWriter in Kestrel (#7110)
This commit is contained in:
parent
7b3149af1e
commit
35b99e44ce
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.IO.Pipelines;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -60,8 +61,73 @@ namespace Microsoft.AspNetCore.Http
|
||||||
throw new ArgumentNullException(nameof(encoding));
|
throw new ArgumentNullException(nameof(encoding));
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] data = encoding.GetBytes(text);
|
// Need to call StartAsync before GetMemory/GetSpan
|
||||||
return response.Body.WriteAsync(data, 0, data.Length, cancellationToken);
|
if (!response.HasStarted)
|
||||||
|
{
|
||||||
|
var startAsyncTask = response.StartAsync(cancellationToken);
|
||||||
|
if (!startAsyncTask.IsCompletedSuccessfully)
|
||||||
|
{
|
||||||
|
return StartAndWriteAsyncAwaited(response, text, encoding, cancellationToken, startAsyncTask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write(response, text, encoding);
|
||||||
|
|
||||||
|
var flushAsyncTask = response.BodyPipe.FlushAsync(cancellationToken);
|
||||||
|
if (flushAsyncTask.IsCompletedSuccessfully)
|
||||||
|
{
|
||||||
|
// Most implementations of ValueTask reset state in GetResult, so call it before returning a completed task.
|
||||||
|
flushAsyncTask.GetAwaiter().GetResult();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
return flushAsyncTask.AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task StartAndWriteAsyncAwaited(this HttpResponse response, string text, Encoding encoding, CancellationToken cancellationToken, Task startAsyncTask)
|
||||||
|
{
|
||||||
|
await startAsyncTask;
|
||||||
|
Write(response, text, encoding);
|
||||||
|
await response.BodyPipe.FlushAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Write(this HttpResponse response, string text, Encoding encoding)
|
||||||
|
{
|
||||||
|
var pipeWriter = response.BodyPipe;
|
||||||
|
var encodedLength = encoding.GetByteCount(text);
|
||||||
|
var destination = pipeWriter.GetSpan(encodedLength);
|
||||||
|
|
||||||
|
if (encodedLength <= destination.Length)
|
||||||
|
{
|
||||||
|
// Just call Encoding.GetBytes if everything will fit into a single segment.
|
||||||
|
var bytesWritten = encoding.GetBytes(text, destination);
|
||||||
|
pipeWriter.Advance(bytesWritten);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WriteMutliSegmentEncoded(pipeWriter, text, encoding, destination, encodedLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteMutliSegmentEncoded(PipeWriter writer, string text, Encoding encoding, Span<byte> destination, int encodedLength)
|
||||||
|
{
|
||||||
|
var encoder = encoding.GetEncoder();
|
||||||
|
var source = text.AsSpan();
|
||||||
|
var completed = false;
|
||||||
|
var totalBytesUsed = 0;
|
||||||
|
|
||||||
|
// This may be a bug, but encoder.Convert returns completed = true for UTF7 too early.
|
||||||
|
// Therefore, we check encodedLength - totalBytesUsed too.
|
||||||
|
while (!completed || encodedLength - totalBytesUsed != 0)
|
||||||
|
{
|
||||||
|
encoder.Convert(source, destination, flush: source.Length == 0, out var charsUsed, out var bytesUsed, out completed);
|
||||||
|
totalBytesUsed += bytesUsed;
|
||||||
|
|
||||||
|
writer.Advance(bytesUsed);
|
||||||
|
source = source.Slice(charsUsed);
|
||||||
|
|
||||||
|
destination = writer.GetSpan(encodedLength - totalBytesUsed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
using System.IO.Pipelines.Tests;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -28,6 +32,65 @@ namespace Microsoft.AspNetCore.Http
|
||||||
Assert.Equal(22, context.Response.Body.Length);
|
Assert.Equal(22, context.Response.Body.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(Encodings))]
|
||||||
|
public async Task WritingTextThatRequiresMultipleSegmentsWorks(Encoding encoding)
|
||||||
|
{
|
||||||
|
// Need to change the StreamPipeWriter with a capped MemoryPool
|
||||||
|
var memoryPool = new TestMemoryPool(maxBufferSize: 16);
|
||||||
|
var outputStream = new MemoryStream();
|
||||||
|
var streamPipeWriter = new StreamPipeWriter(outputStream, minimumSegmentSize: 0, memoryPool);
|
||||||
|
|
||||||
|
HttpContext context = new DefaultHttpContext();
|
||||||
|
context.Response.BodyPipe = streamPipeWriter;
|
||||||
|
|
||||||
|
var inputString = "昨日すき焼きを食べました";
|
||||||
|
var expected = encoding.GetBytes(inputString);
|
||||||
|
await context.Response.WriteAsync(inputString, encoding);
|
||||||
|
|
||||||
|
outputStream.Position = 0;
|
||||||
|
var actual = new byte[expected.Length];
|
||||||
|
var length = outputStream.Read(actual);
|
||||||
|
|
||||||
|
var res1 = encoding.GetString(actual);
|
||||||
|
var res2 = encoding.GetString(expected);
|
||||||
|
Assert.Equal(expected.Length, length);
|
||||||
|
Assert.Equal(expected, actual);
|
||||||
|
streamPipeWriter.Complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(Encodings))]
|
||||||
|
public async Task WritingTextWithPassedInEncodingWorks(Encoding encoding)
|
||||||
|
{
|
||||||
|
HttpContext context = CreateRequest();
|
||||||
|
|
||||||
|
var inputString = "昨日すき焼きを食べました";
|
||||||
|
var expected = encoding.GetBytes(inputString);
|
||||||
|
await context.Response.WriteAsync(inputString, encoding);
|
||||||
|
|
||||||
|
context.Response.Body.Position = 0;
|
||||||
|
var actual = new byte[expected.Length * 2];
|
||||||
|
var length = context.Response.Body.Read(actual);
|
||||||
|
|
||||||
|
var actualShortened = new byte[length];
|
||||||
|
Array.Copy(actual, actualShortened, length);
|
||||||
|
|
||||||
|
Assert.Equal(expected.Length, length);
|
||||||
|
Assert.Equal(expected, actualShortened);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData<Encoding> Encodings =>
|
||||||
|
new TheoryData<Encoding>
|
||||||
|
{
|
||||||
|
{ Encoding.ASCII },
|
||||||
|
{ Encoding.BigEndianUnicode },
|
||||||
|
{ Encoding.Unicode },
|
||||||
|
{ Encoding.UTF32 },
|
||||||
|
{ Encoding.UTF7 },
|
||||||
|
{ Encoding.UTF8 }
|
||||||
|
};
|
||||||
|
|
||||||
private HttpContext CreateRequest()
|
private HttpContext CreateRequest()
|
||||||
{
|
{
|
||||||
HttpContext context = new DefaultHttpContext();
|
HttpContext context = new DefaultHttpContext();
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@
|
||||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\Http\test\Microsoft.AspNetCore.Http.Tests.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Microsoft.AspNetCore.Http" />
|
<Reference Include="Microsoft.AspNetCore.Http" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -95,4 +95,4 @@ namespace Microsoft.AspNetCore.Http.Features
|
||||||
public TFeature Fetch<TFeature>(ref TFeature cached, Func<IFeatureCollection, TFeature> factory)
|
public TFeature Fetch<TFeature>(ref TFeature cached, Func<IFeatureCollection, TFeature> factory)
|
||||||
where TFeature : class => Fetch(ref cached, Collection, factory);
|
where TFeature : class => Fetch(ref cached, Collection, factory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@ namespace Microsoft.AspNetCore.Http.Internal
|
||||||
return HttpResponseFeature.Body.FlushAsync(cancellationToken);
|
return HttpResponseFeature.Body.FlushAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
return HttpResponseStartFeature.StartAsync();
|
return HttpResponseStartFeature.StartAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FeatureInterfaces
|
struct FeatureInterfaces
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ namespace System.IO.Pipelines
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ReadOnlyPipeStream : Stream
|
public class ReadOnlyPipeStream : Stream
|
||||||
{
|
{
|
||||||
private readonly PipeReader _pipeReader;
|
|
||||||
private bool _allowSynchronousIO = true;
|
private bool _allowSynchronousIO = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -33,7 +32,7 @@ namespace System.IO.Pipelines
|
||||||
public ReadOnlyPipeStream(PipeReader pipeReader, bool allowSynchronousIO)
|
public ReadOnlyPipeStream(PipeReader pipeReader, bool allowSynchronousIO)
|
||||||
{
|
{
|
||||||
_allowSynchronousIO = allowSynchronousIO;
|
_allowSynchronousIO = allowSynchronousIO;
|
||||||
_pipeReader = pipeReader;
|
InnerPipeReader = pipeReader;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -62,6 +61,8 @@ namespace System.IO.Pipelines
|
||||||
set => throw new NotSupportedException();
|
set => throw new NotSupportedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PipeReader InnerPipeReader { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Write(byte[] buffer, int offset, int count)
|
public override void Write(byte[] buffer, int offset, int count)
|
||||||
=> throw new NotSupportedException();
|
=> throw new NotSupportedException();
|
||||||
|
|
@ -160,7 +161,7 @@ namespace System.IO.Pipelines
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var result = await _pipeReader.ReadAsync(cancellationToken);
|
var result = await InnerPipeReader.ReadAsync(cancellationToken);
|
||||||
var readableBuffer = result.Buffer;
|
var readableBuffer = result.Buffer;
|
||||||
var readableBufferLength = readableBuffer.Length;
|
var readableBufferLength = readableBuffer.Length;
|
||||||
|
|
||||||
|
|
@ -186,7 +187,7 @@ namespace System.IO.Pipelines
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_pipeReader.AdvanceTo(consumed);
|
InnerPipeReader.AdvanceTo(consumed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -211,7 +212,7 @@ namespace System.IO.Pipelines
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var result = await _pipeReader.ReadAsync(cancellationToken);
|
var result = await InnerPipeReader.ReadAsync(cancellationToken);
|
||||||
var readableBuffer = result.Buffer;
|
var readableBuffer = result.Buffer;
|
||||||
var readableBufferLength = readableBuffer.Length;
|
var readableBufferLength = readableBuffer.Length;
|
||||||
|
|
||||||
|
|
@ -232,7 +233,7 @@ namespace System.IO.Pipelines
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_pipeReader.AdvanceTo(readableBuffer.End);
|
InnerPipeReader.AdvanceTo(readableBuffer.End);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ namespace System.IO.Pipelines
|
||||||
{
|
{
|
||||||
private readonly int _minimumSegmentSize;
|
private readonly int _minimumSegmentSize;
|
||||||
private readonly int _minimumReadThreshold;
|
private readonly int _minimumReadThreshold;
|
||||||
private readonly Stream _readingStream;
|
|
||||||
private readonly MemoryPool<byte> _pool;
|
private readonly MemoryPool<byte> _pool;
|
||||||
|
|
||||||
private CancellationTokenSource _internalTokenSource;
|
private CancellationTokenSource _internalTokenSource;
|
||||||
|
|
@ -42,7 +41,6 @@ namespace System.IO.Pipelines
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new StreamPipeReader.
|
/// Creates a new StreamPipeReader.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -50,7 +48,7 @@ namespace System.IO.Pipelines
|
||||||
/// <param name="options">The options to use.</param>
|
/// <param name="options">The options to use.</param>
|
||||||
public StreamPipeReader(Stream readingStream, StreamPipeReaderOptions options)
|
public StreamPipeReader(Stream readingStream, StreamPipeReaderOptions options)
|
||||||
{
|
{
|
||||||
_readingStream = readingStream ?? throw new ArgumentNullException(nameof(readingStream));
|
InnerStream = readingStream ?? throw new ArgumentNullException(nameof(readingStream));
|
||||||
|
|
||||||
if (options == null)
|
if (options == null)
|
||||||
{
|
{
|
||||||
|
|
@ -70,7 +68,7 @@ namespace System.IO.Pipelines
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the inner stream that is being read from.
|
/// Gets the inner stream that is being read from.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Stream InnerStream => _readingStream;
|
public Stream InnerStream { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void AdvanceTo(SequencePosition consumed)
|
public override void AdvanceTo(SequencePosition consumed)
|
||||||
|
|
@ -235,7 +233,7 @@ namespace System.IO.Pipelines
|
||||||
{
|
{
|
||||||
AllocateReadTail();
|
AllocateReadTail();
|
||||||
#if NETCOREAPP3_0
|
#if NETCOREAPP3_0
|
||||||
var length = await _readingStream.ReadAsync(_readTail.AvailableMemory.Slice(_readTail.End), tokenSource.Token);
|
var length = await InnerStream.ReadAsync(_readTail.AvailableMemory.Slice(_readTail.End), tokenSource.Token);
|
||||||
#elif NETSTANDARD2_0
|
#elif NETSTANDARD2_0
|
||||||
if (!MemoryMarshal.TryGetArray<byte>(_readTail.AvailableMemory.Slice(_readTail.End), out var arraySegment))
|
if (!MemoryMarshal.TryGetArray<byte>(_readTail.AvailableMemory.Slice(_readTail.End), out var arraySegment))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ namespace System.IO.Pipelines
|
||||||
public class StreamPipeWriter : PipeWriter, IDisposable
|
public class StreamPipeWriter : PipeWriter, IDisposable
|
||||||
{
|
{
|
||||||
private readonly int _minimumSegmentSize;
|
private readonly int _minimumSegmentSize;
|
||||||
private readonly Stream _writingStream;
|
|
||||||
private int _bytesWritten;
|
private int _bytesWritten;
|
||||||
|
|
||||||
private List<CompletedBuffer> _completedSegments;
|
private List<CompletedBuffer> _completedSegments;
|
||||||
|
|
@ -53,14 +52,14 @@ namespace System.IO.Pipelines
|
||||||
public StreamPipeWriter(Stream writingStream, int minimumSegmentSize, MemoryPool<byte> pool = null)
|
public StreamPipeWriter(Stream writingStream, int minimumSegmentSize, MemoryPool<byte> pool = null)
|
||||||
{
|
{
|
||||||
_minimumSegmentSize = minimumSegmentSize;
|
_minimumSegmentSize = minimumSegmentSize;
|
||||||
_writingStream = writingStream;
|
InnerStream = writingStream;
|
||||||
_pool = pool ?? MemoryPool<byte>.Shared;
|
_pool = pool ?? MemoryPool<byte>.Shared;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the inner stream that is being written to.
|
/// Gets the inner stream that is being written to.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Stream InnerStream => _writingStream;
|
public Stream InnerStream { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Advance(int count)
|
public override void Advance(int count)
|
||||||
|
|
@ -180,10 +179,10 @@ namespace System.IO.Pipelines
|
||||||
{
|
{
|
||||||
var segment = _completedSegments[0];
|
var segment = _completedSegments[0];
|
||||||
#if NETCOREAPP3_0
|
#if NETCOREAPP3_0
|
||||||
await _writingStream.WriteAsync(segment.Buffer.Slice(0, segment.Length), localToken);
|
await InnerStream.WriteAsync(segment.Buffer.Slice(0, segment.Length), localToken);
|
||||||
#elif NETSTANDARD2_0
|
#elif NETSTANDARD2_0
|
||||||
MemoryMarshal.TryGetArray<byte>(segment.Buffer, out var arraySegment);
|
MemoryMarshal.TryGetArray<byte>(segment.Buffer, out var arraySegment);
|
||||||
await _writingStream.WriteAsync(arraySegment.Array, 0, segment.Length, localToken);
|
await InnerStream.WriteAsync(arraySegment.Array, 0, segment.Length, localToken);
|
||||||
#else
|
#else
|
||||||
#error Target frameworks need to be updated.
|
#error Target frameworks need to be updated.
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -196,10 +195,10 @@ namespace System.IO.Pipelines
|
||||||
if (!_currentSegment.IsEmpty)
|
if (!_currentSegment.IsEmpty)
|
||||||
{
|
{
|
||||||
#if NETCOREAPP3_0
|
#if NETCOREAPP3_0
|
||||||
await _writingStream.WriteAsync(_currentSegment.Slice(0, _position), localToken);
|
await InnerStream.WriteAsync(_currentSegment.Slice(0, _position), localToken);
|
||||||
#elif NETSTANDARD2_0
|
#elif NETSTANDARD2_0
|
||||||
MemoryMarshal.TryGetArray<byte>(_currentSegment, out var arraySegment);
|
MemoryMarshal.TryGetArray<byte>(_currentSegment, out var arraySegment);
|
||||||
await _writingStream.WriteAsync(arraySegment.Array, 0, _position, localToken);
|
await InnerStream.WriteAsync(arraySegment.Array, 0, _position, localToken);
|
||||||
#else
|
#else
|
||||||
#error Target frameworks need to be updated.
|
#error Target frameworks need to be updated.
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -207,7 +206,7 @@ namespace System.IO.Pipelines
|
||||||
_position = 0;
|
_position = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _writingStream.FlushAsync(localToken);
|
await InnerStream.FlushAsync(localToken);
|
||||||
|
|
||||||
return new FlushResult(isCanceled: false, _isCompleted);
|
return new FlushResult(isCanceled: false, _isCompleted);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ namespace System.IO.Pipelines
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class WriteOnlyPipeStream : Stream
|
public class WriteOnlyPipeStream : Stream
|
||||||
{
|
{
|
||||||
private PipeWriter _pipeWriter;
|
|
||||||
private bool _allowSynchronousIO = true;
|
private bool _allowSynchronousIO = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -30,7 +29,7 @@ namespace System.IO.Pipelines
|
||||||
/// <param name="allowSynchronousIO">Whether synchronous IO is allowed.</param>
|
/// <param name="allowSynchronousIO">Whether synchronous IO is allowed.</param>
|
||||||
public WriteOnlyPipeStream(PipeWriter pipeWriter, bool allowSynchronousIO)
|
public WriteOnlyPipeStream(PipeWriter pipeWriter, bool allowSynchronousIO)
|
||||||
{
|
{
|
||||||
_pipeWriter = pipeWriter;
|
InnerPipeWriter = pipeWriter;
|
||||||
_allowSynchronousIO = allowSynchronousIO;
|
_allowSynchronousIO = allowSynchronousIO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,6 +59,8 @@ namespace System.IO.Pipelines
|
||||||
set => throw new NotSupportedException();
|
set => throw new NotSupportedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PipeWriter InnerPipeWriter { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override int Read(byte[] buffer, int offset, int count)
|
public override int Read(byte[] buffer, int offset, int count)
|
||||||
=> throw new NotSupportedException();
|
=> throw new NotSupportedException();
|
||||||
|
|
@ -82,7 +83,7 @@ namespace System.IO.Pipelines
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task FlushAsync(CancellationToken cancellationToken)
|
public override async Task FlushAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await _pipeWriter.FlushAsync(cancellationToken);
|
await InnerPipeWriter.FlushAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -150,13 +151,27 @@ namespace System.IO.Pipelines
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return WriteAsync(new ReadOnlyMemory<byte>(buffer, offset, count), cancellationToken).AsTask();
|
return WriteAsyncInternal(new ReadOnlyMemory<byte>(buffer, offset, count), cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
|
public override ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await _pipeWriter.WriteAsync(source, cancellationToken);
|
return new ValueTask(WriteAsyncInternal(source, cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task WriteAsyncInternal(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var task = InnerPipeWriter.WriteAsync(source, cancellationToken);
|
||||||
|
|
||||||
|
if (task.IsCompletedSuccessfully)
|
||||||
|
{
|
||||||
|
// Most ValueTask implementations reset in GetResult, so call it before returning completed task
|
||||||
|
task.GetAwaiter().GetResult();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
return task.AsTask();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,22 @@ namespace Microsoft.AspNetCore.Http.Internal
|
||||||
mock.Verify(m => m.StartAsync(default), Times.Once());
|
mock.Verify(m => m.StartAsync(default), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResponseStart_CallsFeatureIfSetWithProvidedCancellationToken()
|
||||||
|
{
|
||||||
|
var features = new FeatureCollection();
|
||||||
|
|
||||||
|
var mock = new Mock<IHttpResponseStartFeature>();
|
||||||
|
var ct = new CancellationToken();
|
||||||
|
mock.Setup(o => o.StartAsync(It.Is<CancellationToken>((localCt) => localCt.Equals(ct)))).Returns(Task.CompletedTask);
|
||||||
|
features.Set(mock.Object);
|
||||||
|
|
||||||
|
var context = new DefaultHttpContext(features);
|
||||||
|
await context.Response.StartAsync(ct);
|
||||||
|
|
||||||
|
mock.Verify(m => m.StartAsync(default), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ResponseStart_CallsResponseBodyFlushIfNotSet()
|
public async Task ResponseStart_CallsResponseBodyFlushIfNotSet()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,13 @@ namespace System.IO.Pipelines.Tests
|
||||||
Assert.Throws<InvalidOperationException>(() => readOnlyPipeStream.Read(new byte[0], 0, 0));
|
Assert.Throws<InvalidOperationException>(() => readOnlyPipeStream.Read(new byte[0], 0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InnerPipeReaderReturnsPipeReader()
|
||||||
|
{
|
||||||
|
var readOnlyPipeStream = new ReadOnlyPipeStream(Reader, allowSynchronousIO: false);
|
||||||
|
Assert.Equal(Reader, readOnlyPipeStream.InnerPipeReader);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<Mock<PipeReader>> SetupMockPipeReader()
|
private async Task<Mock<PipeReader>> SetupMockPipeReader()
|
||||||
{
|
{
|
||||||
await WriteByteArrayToPipeAsync(new byte[1]);
|
await WriteByteArrayToPipeAsync(new byte[1]);
|
||||||
|
|
|
||||||
|
|
@ -611,6 +611,12 @@ namespace System.IO.Pipelines.Tests
|
||||||
Assert.True(readResult.IsCompleted);
|
Assert.True(readResult.IsCompleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InnerStreamReturnsStream()
|
||||||
|
{
|
||||||
|
Assert.Equal(Stream, ((StreamPipeReader)Reader).InnerStream);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<string> ReadFromPipeAsString()
|
private async Task<string> ReadFromPipeAsString()
|
||||||
{
|
{
|
||||||
var readResult = await Reader.ReadAsync();
|
var readResult = await Reader.ReadAsync();
|
||||||
|
|
|
||||||
|
|
@ -25,27 +25,22 @@ namespace System.IO.Pipelines.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(100, 1000)]
|
[InlineData(100)]
|
||||||
[InlineData(100, 8000)]
|
[InlineData(4000)]
|
||||||
[InlineData(100, 10000)]
|
public async Task CanAdvanceWithPartialConsumptionOfFirstSegment(int firstWriteLength)
|
||||||
[InlineData(8000, 100)]
|
|
||||||
[InlineData(8000, 8000)]
|
|
||||||
public async Task CanAdvanceWithPartialConsumptionOfFirstSegment(int firstWriteLength, int secondWriteLength)
|
|
||||||
{
|
{
|
||||||
Writer = new StreamPipeWriter(Stream, MinimumSegmentSize, new TestMemoryPool(maxBufferSize: 20000));
|
Writer = new StreamPipeWriter(Stream, MinimumSegmentSize, new TestMemoryPool(maxBufferSize: 20000));
|
||||||
await Writer.WriteAsync(Encoding.ASCII.GetBytes("a"));
|
await Writer.WriteAsync(Encoding.ASCII.GetBytes("a"));
|
||||||
|
|
||||||
var expectedLength = firstWriteLength + secondWriteLength + 1;
|
|
||||||
|
|
||||||
var memory = Writer.GetMemory(firstWriteLength);
|
var memory = Writer.GetMemory(firstWriteLength);
|
||||||
Writer.Advance(firstWriteLength);
|
Writer.Advance(firstWriteLength);
|
||||||
|
|
||||||
memory = Writer.GetMemory(secondWriteLength);
|
memory = Writer.GetMemory();
|
||||||
Writer.Advance(secondWriteLength);
|
Writer.Advance(memory.Length);
|
||||||
|
|
||||||
await Writer.FlushAsync();
|
await Writer.FlushAsync();
|
||||||
|
|
||||||
Assert.Equal(expectedLength, Read().Length);
|
Assert.Equal(firstWriteLength + memory.Length + 1, Read().Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -408,6 +403,12 @@ namespace System.IO.Pipelines.Tests
|
||||||
Assert.Equal(expectedString, ReadAsString());
|
Assert.Equal(expectedString, ReadAsString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InnerStreamReturnsStream()
|
||||||
|
{
|
||||||
|
Assert.Equal(Stream, ((StreamPipeWriter)Writer).InnerStream);
|
||||||
|
}
|
||||||
|
|
||||||
private void WriteStringToStream(string input)
|
private void WriteStringToStream(string input)
|
||||||
{
|
{
|
||||||
var buffer = Encoding.ASCII.GetBytes(input);
|
var buffer = Encoding.ASCII.GetBytes(input);
|
||||||
|
|
|
||||||
|
|
@ -195,5 +195,12 @@ namespace System.IO.Pipelines.Tests
|
||||||
Assert.Throws<InvalidOperationException>(() => writeOnlyPipeStream.Write(new byte[0], 0, 0));
|
Assert.Throws<InvalidOperationException>(() => writeOnlyPipeStream.Write(new byte[0], 0, 0));
|
||||||
Assert.Throws<InvalidOperationException>(() => writeOnlyPipeStream.Flush());
|
Assert.Throws<InvalidOperationException>(() => writeOnlyPipeStream.Flush());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InnerPipeWriterReturnsPipeWriter()
|
||||||
|
{
|
||||||
|
var writeOnlyPipeStream = new WriteOnlyPipeStream(Writer, allowSynchronousIO: false);
|
||||||
|
Assert.Equal(Writer, writeOnlyPipeStream.InnerPipeWriter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,14 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stream wrapper that create specific compression stream only if necessary.
|
/// Stream wrapper that create specific compression stream only if necessary.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class BodyWrapperStream : Stream, IHttpBufferingFeature, IHttpSendFileFeature
|
internal class BodyWrapperStream : Stream, IHttpBufferingFeature, IHttpSendFileFeature, IHttpResponseStartFeature
|
||||||
{
|
{
|
||||||
private readonly HttpContext _context;
|
private readonly HttpContext _context;
|
||||||
private readonly Stream _bodyOriginalStream;
|
private readonly Stream _bodyOriginalStream;
|
||||||
private readonly IResponseCompressionProvider _provider;
|
private readonly IResponseCompressionProvider _provider;
|
||||||
private readonly IHttpBufferingFeature _innerBufferFeature;
|
private readonly IHttpBufferingFeature _innerBufferFeature;
|
||||||
private readonly IHttpSendFileFeature _innerSendFileFeature;
|
private readonly IHttpSendFileFeature _innerSendFileFeature;
|
||||||
|
private readonly IHttpResponseStartFeature _innerStartFeature;
|
||||||
|
|
||||||
private ICompressionProvider _compressionProvider = null;
|
private ICompressionProvider _compressionProvider = null;
|
||||||
private bool _compressionChecked = false;
|
private bool _compressionChecked = false;
|
||||||
|
|
@ -30,13 +31,14 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
private bool _autoFlush = false;
|
private bool _autoFlush = false;
|
||||||
|
|
||||||
internal BodyWrapperStream(HttpContext context, Stream bodyOriginalStream, IResponseCompressionProvider provider,
|
internal BodyWrapperStream(HttpContext context, Stream bodyOriginalStream, IResponseCompressionProvider provider,
|
||||||
IHttpBufferingFeature innerBufferFeature, IHttpSendFileFeature innerSendFileFeature)
|
IHttpBufferingFeature innerBufferFeature, IHttpSendFileFeature innerSendFileFeature, IHttpResponseStartFeature innerStartFeature)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_bodyOriginalStream = bodyOriginalStream;
|
_bodyOriginalStream = bodyOriginalStream;
|
||||||
_provider = provider;
|
_provider = provider;
|
||||||
_innerBufferFeature = innerBufferFeature;
|
_innerBufferFeature = innerBufferFeature;
|
||||||
_innerSendFileFeature = innerSendFileFeature;
|
_innerSendFileFeature = innerSendFileFeature;
|
||||||
|
_innerStartFeature = innerStartFeature;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal ValueTask FinishCompressionAsync()
|
internal ValueTask FinishCompressionAsync()
|
||||||
|
|
@ -318,5 +320,17 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken token = default)
|
||||||
|
{
|
||||||
|
OnWrite();
|
||||||
|
|
||||||
|
if (_innerStartFeature != null)
|
||||||
|
{
|
||||||
|
return _innerStartFeature.StartAsync(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCompression
|
namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
{
|
{
|
||||||
|
|
@ -55,9 +54,10 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
var bodyStream = context.Response.Body;
|
var bodyStream = context.Response.Body;
|
||||||
var originalBufferFeature = context.Features.Get<IHttpBufferingFeature>();
|
var originalBufferFeature = context.Features.Get<IHttpBufferingFeature>();
|
||||||
var originalSendFileFeature = context.Features.Get<IHttpSendFileFeature>();
|
var originalSendFileFeature = context.Features.Get<IHttpSendFileFeature>();
|
||||||
|
var originalStartFeature = context.Features.Get<IHttpResponseStartFeature>();
|
||||||
|
|
||||||
var bodyWrapperStream = new BodyWrapperStream(context, bodyStream, _provider,
|
var bodyWrapperStream = new BodyWrapperStream(context, bodyStream, _provider,
|
||||||
originalBufferFeature, originalSendFileFeature);
|
originalBufferFeature, originalSendFileFeature, originalStartFeature);
|
||||||
context.Response.Body = bodyWrapperStream;
|
context.Response.Body = bodyWrapperStream;
|
||||||
context.Features.Set<IHttpBufferingFeature>(bodyWrapperStream);
|
context.Features.Set<IHttpBufferingFeature>(bodyWrapperStream);
|
||||||
if (originalSendFileFeature != null)
|
if (originalSendFileFeature != null)
|
||||||
|
|
@ -65,6 +65,11 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
context.Features.Set<IHttpSendFileFeature>(bodyWrapperStream);
|
context.Features.Set<IHttpSendFileFeature>(bodyWrapperStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (originalStartFeature != null)
|
||||||
|
{
|
||||||
|
context.Features.Set<IHttpResponseStartFeature>(bodyWrapperStream);
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _next(context);
|
await _next(context);
|
||||||
|
|
@ -78,6 +83,11 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
{
|
{
|
||||||
context.Features.Set(originalSendFileFeature);
|
context.Features.Set(originalSendFileFeature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (originalStartFeature != null)
|
||||||
|
{
|
||||||
|
context.Features.Set(originalStartFeature);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
{
|
{
|
||||||
var httpContext = new DefaultHttpContext();
|
var httpContext = new DefaultHttpContext();
|
||||||
httpContext.Response.Headers[HeaderNames.Vary] = providedVaryHeader;
|
httpContext.Response.Headers[HeaderNames.Vary] = providedVaryHeader;
|
||||||
var stream = new BodyWrapperStream(httpContext, new MemoryStream(), new MockResponseCompressionProvider(flushable: true), null, null);
|
var stream = new BodyWrapperStream(httpContext, new MemoryStream(), new MockResponseCompressionProvider(flushable: true), null, null, null);
|
||||||
|
|
||||||
stream.Write(new byte[] { }, 0, 0);
|
stream.Write(new byte[] { }, 0, 0);
|
||||||
|
|
||||||
|
|
@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
written = new ArraySegment<byte>(b, 0, c).ToArray();
|
written = new ArraySegment<byte>(b, 0, c).ToArray();
|
||||||
});
|
});
|
||||||
|
|
||||||
var stream = new BodyWrapperStream(new DefaultHttpContext(), mock.Object, new MockResponseCompressionProvider(flushable), null, null);
|
var stream = new BodyWrapperStream(new DefaultHttpContext(), mock.Object, new MockResponseCompressionProvider(flushable), null, null, null);
|
||||||
|
|
||||||
stream.DisableResponseBuffering();
|
stream.DisableResponseBuffering();
|
||||||
stream.Write(buffer, 0, buffer.Length);
|
stream.Write(buffer, 0, buffer.Length);
|
||||||
|
|
@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
var buffer = new byte[] { 1 };
|
var buffer = new byte[] { 1 };
|
||||||
|
|
||||||
var memoryStream = new MemoryStream();
|
var memoryStream = new MemoryStream();
|
||||||
var stream = new BodyWrapperStream(new DefaultHttpContext(), memoryStream, new MockResponseCompressionProvider(flushable), null, null);
|
var stream = new BodyWrapperStream(new DefaultHttpContext(), memoryStream, new MockResponseCompressionProvider(flushable), null, null, null);
|
||||||
|
|
||||||
stream.DisableResponseBuffering();
|
stream.DisableResponseBuffering();
|
||||||
await stream.WriteAsync(buffer, 0, buffer.Length);
|
await stream.WriteAsync(buffer, 0, buffer.Length);
|
||||||
|
|
@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
{
|
{
|
||||||
var memoryStream = new MemoryStream();
|
var memoryStream = new MemoryStream();
|
||||||
|
|
||||||
var stream = new BodyWrapperStream(new DefaultHttpContext(), memoryStream, new MockResponseCompressionProvider(true), null, null);
|
var stream = new BodyWrapperStream(new DefaultHttpContext(), memoryStream, new MockResponseCompressionProvider(true), null, null, null);
|
||||||
|
|
||||||
stream.DisableResponseBuffering();
|
stream.DisableResponseBuffering();
|
||||||
|
|
||||||
|
|
@ -99,7 +99,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
|
|
||||||
var memoryStream = new MemoryStream();
|
var memoryStream = new MemoryStream();
|
||||||
|
|
||||||
var stream = new BodyWrapperStream(new DefaultHttpContext(), memoryStream, new MockResponseCompressionProvider(flushable), null, null);
|
var stream = new BodyWrapperStream(new DefaultHttpContext(), memoryStream, new MockResponseCompressionProvider(flushable), null, null, null);
|
||||||
|
|
||||||
stream.DisableResponseBuffering();
|
stream.DisableResponseBuffering();
|
||||||
stream.BeginWrite(buffer, 0, buffer.Length, (o) => {}, null);
|
stream.BeginWrite(buffer, 0, buffer.Length, (o) => {}, null);
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,8 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
yield return new EncodingTestData("gzip", expectedBodyLength: 24);
|
yield return new EncodingTestData("gzip", expectedBodyLength: 30);
|
||||||
yield return new EncodingTestData("br", expectedBodyLength: 20);
|
yield return new EncodingTestData("br", expectedBodyLength: 21);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
{
|
{
|
||||||
var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "gzip", "deflate" }, responseType: TextPlain);
|
var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "gzip", "deflate" }, responseType: TextPlain);
|
||||||
|
|
||||||
CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip");
|
CheckResponseCompressed(response, expectedBodyLength: 30, expectedEncoding: "gzip");
|
||||||
AssertCompressedWithLog(logMessages, "gzip");
|
AssertCompressedWithLog(logMessages, "gzip");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,7 +73,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
{
|
{
|
||||||
var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "br" }, responseType: TextPlain);
|
var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "br" }, responseType: TextPlain);
|
||||||
|
|
||||||
CheckResponseCompressed(response, expectedBodyLength: 20, expectedEncoding: "br");
|
CheckResponseCompressed(response, expectedBodyLength: 21, expectedEncoding: "br");
|
||||||
AssertCompressedWithLog(logMessages, "br");
|
AssertCompressedWithLog(logMessages, "br");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,7 +84,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
{
|
{
|
||||||
var (response, logMessages) = await InvokeMiddleware(100, new[] { encoding1, encoding2 }, responseType: TextPlain);
|
var (response, logMessages) = await InvokeMiddleware(100, new[] { encoding1, encoding2 }, responseType: TextPlain);
|
||||||
|
|
||||||
CheckResponseCompressed(response, expectedBodyLength: 20, expectedEncoding: "br");
|
CheckResponseCompressed(response, expectedBodyLength: 21, expectedEncoding: "br");
|
||||||
AssertCompressedWithLog(logMessages, "br");
|
AssertCompressedWithLog(logMessages, "br");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
|
|
||||||
var (response, logMessages) = await InvokeMiddleware(100, new[] { encoding1, encoding2 }, responseType: TextPlain, configure: Configure);
|
var (response, logMessages) = await InvokeMiddleware(100, new[] { encoding1, encoding2 }, responseType: TextPlain, configure: Configure);
|
||||||
|
|
||||||
CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip");
|
CheckResponseCompressed(response, expectedBodyLength: 30, expectedEncoding: "gzip");
|
||||||
AssertCompressedWithLog(logMessages, "gzip");
|
AssertCompressedWithLog(logMessages, "gzip");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,7 +126,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
{
|
{
|
||||||
var (response, logMessages) = await InvokeMiddleware(uncompressedBodyLength: 100, requestAcceptEncodings: new[] { "gzip" }, contentType);
|
var (response, logMessages) = await InvokeMiddleware(uncompressedBodyLength: 100, requestAcceptEncodings: new[] { "gzip" }, contentType);
|
||||||
|
|
||||||
CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip");
|
CheckResponseCompressed(response, expectedBodyLength: 30, expectedEncoding: "gzip");
|
||||||
AssertCompressedWithLog(logMessages, "gzip");
|
AssertCompressedWithLog(logMessages, "gzip");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,7 +158,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
|
|
||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
|
|
||||||
CheckResponseCompressed(response, expectedBodyLength: 123, expectedEncoding: "gzip");
|
CheckResponseCompressed(response, expectedBodyLength: 133, expectedEncoding: "gzip");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -251,7 +251,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
|
|
||||||
if (compress)
|
if (compress)
|
||||||
{
|
{
|
||||||
CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip");
|
CheckResponseCompressed(response, expectedBodyLength: 30, expectedEncoding: "gzip");
|
||||||
AssertCompressedWithLog(logMessages, "gzip");
|
AssertCompressedWithLog(logMessages, "gzip");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -272,7 +272,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
options.ExcludedMimeTypes = new[] { "text/*" };
|
options.ExcludedMimeTypes = new[] { "text/*" };
|
||||||
});
|
});
|
||||||
|
|
||||||
CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip");
|
CheckResponseCompressed(response, expectedBodyLength: 30, expectedEncoding: "gzip");
|
||||||
AssertCompressedWithLog(logMessages, "gzip");
|
AssertCompressedWithLog(logMessages, "gzip");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -317,7 +317,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
{
|
{
|
||||||
var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "*" }, responseType: TextPlain);
|
var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "*" }, responseType: TextPlain);
|
||||||
|
|
||||||
CheckResponseCompressed(response, expectedBodyLength: 20, expectedEncoding: "br");
|
CheckResponseCompressed(response, expectedBodyLength: 21, expectedEncoding: "br");
|
||||||
AssertCompressedWithLog(logMessages, "br");
|
AssertCompressedWithLog(logMessages, "br");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -334,9 +334,9 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(new[] { "identity;q=0.5", "gzip;q=1" }, 24)]
|
[InlineData(new[] { "identity;q=0.5", "gzip;q=1" }, 30)]
|
||||||
[InlineData(new[] { "identity;q=0", "gzip;q=0.8" }, 24)]
|
[InlineData(new[] { "identity;q=0", "gzip;q=0.8" }, 30)]
|
||||||
[InlineData(new[] { "identity;q=0.5", "gzip" }, 24)]
|
[InlineData(new[] { "identity;q=0.5", "gzip" }, 30)]
|
||||||
public async Task Request_AcceptWithHigherCompressionQuality_Compressed(string[] acceptEncodings, int expectedBodyLength)
|
public async Task Request_AcceptWithHigherCompressionQuality_Compressed(string[] acceptEncodings, int expectedBodyLength)
|
||||||
{
|
{
|
||||||
var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: acceptEncodings, responseType: TextPlain);
|
var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: acceptEncodings, responseType: TextPlain);
|
||||||
|
|
@ -404,7 +404,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(false, 100)]
|
[InlineData(false, 100)]
|
||||||
[InlineData(true, 24)]
|
[InlineData(true, 30)]
|
||||||
public async Task Request_Https_CompressedIfEnabled(bool enableHttps, int expectedLength)
|
public async Task Request_Https_CompressedIfEnabled(bool enableHttps, int expectedLength)
|
||||||
{
|
{
|
||||||
var sink = new TestSink(
|
var sink = new TestSink(
|
||||||
|
|
@ -908,7 +908,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
|
|
||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
|
|
||||||
CheckResponseCompressed(response, expectedBodyLength: 40, expectedEncoding: "gzip");
|
CheckResponseCompressed(response, expectedBodyLength: 46, expectedEncoding: "gzip");
|
||||||
|
|
||||||
Assert.False(fakeSendFile.Invoked);
|
Assert.False(fakeSendFile.Invoked);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
|
||||||
|
|
@ -596,4 +596,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
|
||||||
<data name="ServerShutdownDuringConnectionInitialization" xml:space="preserve">
|
<data name="ServerShutdownDuringConnectionInitialization" xml:space="preserve">
|
||||||
<value>Server shutdown started during connection initialization.</value>
|
<value>Server shutdown started during connection initialization.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="StartAsyncBeforeGetMemory" xml:space="preserve">
|
||||||
|
<value>Cannot call GetMemory() until response has started. Call HttpResponse.StartAsync() before calling GetMemory().</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -44,13 +44,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void WriteBeginChunkBytes(this ref BufferWriter<PipeWriter> start, int dataCount)
|
internal static int WriteBeginChunkBytes(this ref BufferWriter<PipeWriter> start, int dataCount)
|
||||||
{
|
{
|
||||||
// 10 bytes is max length + \r\n
|
// 10 bytes is max length + \r\n
|
||||||
start.Ensure(10);
|
start.Ensure(10);
|
||||||
|
|
||||||
var count = BeginChunkBytes(dataCount, start.Span);
|
var count = BeginChunkBytes(dataCount, start.Span);
|
||||||
start.Advance(count);
|
start.Advance(count);
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void WriteEndChunkBytes(this ref BufferWriter<PipeWriter> start)
|
internal static void WriteEndChunkBytes(this ref BufferWriter<PipeWriter> start)
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
_context.ConnectionContext,
|
_context.ConnectionContext,
|
||||||
_context.ServiceContext.Log,
|
_context.ServiceContext.Log,
|
||||||
_context.TimeoutControl,
|
_context.TimeoutControl,
|
||||||
this);
|
this,
|
||||||
|
_context.MemoryPool);
|
||||||
|
|
||||||
Input = _context.Transport.Input;
|
Input = _context.Transport.Input;
|
||||||
Output = _http1Output;
|
Output = _http1Output;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
@ -25,15 +26,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
private readonly IKestrelTrace _log;
|
private readonly IKestrelTrace _log;
|
||||||
private readonly IHttpMinResponseDataRateFeature _minResponseDataRateFeature;
|
private readonly IHttpMinResponseDataRateFeature _minResponseDataRateFeature;
|
||||||
private readonly TimingPipeFlusher _flusher;
|
private readonly TimingPipeFlusher _flusher;
|
||||||
|
private readonly MemoryPool<byte> _memoryPool;
|
||||||
|
|
||||||
// This locks access to to all of the below fields
|
// This locks access to all of the below fields
|
||||||
private readonly object _contextLock = new object();
|
private readonly object _contextLock = new object();
|
||||||
|
|
||||||
private bool _completed = false;
|
private bool _pipeWriterCompleted;
|
||||||
|
private bool _completed;
|
||||||
private bool _aborted;
|
private bool _aborted;
|
||||||
private long _unflushedBytes;
|
private long _unflushedBytes;
|
||||||
|
private bool _autoChunk;
|
||||||
private readonly PipeWriter _pipeWriter;
|
private readonly PipeWriter _pipeWriter;
|
||||||
|
private const int MemorySizeThreshold = 1024;
|
||||||
|
private const int BeginChunkLengthMax = 5;
|
||||||
|
private const int EndChunkLength = 2;
|
||||||
|
|
||||||
|
// 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 int _advancedBytesForChunk;
|
||||||
|
private Memory<byte> _currentChunkMemory;
|
||||||
|
private bool _currentChunkMemoryUpdated;
|
||||||
|
private IMemoryOwner<byte> _fakeMemoryOwner;
|
||||||
|
|
||||||
public Http1OutputProducer(
|
public Http1OutputProducer(
|
||||||
PipeWriter pipeWriter,
|
PipeWriter pipeWriter,
|
||||||
|
|
@ -41,7 +57,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
ConnectionContext connectionContext,
|
ConnectionContext connectionContext,
|
||||||
IKestrelTrace log,
|
IKestrelTrace log,
|
||||||
ITimeoutControl timeoutControl,
|
ITimeoutControl timeoutControl,
|
||||||
IHttpMinResponseDataRateFeature minResponseDataRateFeature)
|
IHttpMinResponseDataRateFeature minResponseDataRateFeature,
|
||||||
|
MemoryPool<byte> memoryPool)
|
||||||
{
|
{
|
||||||
_pipeWriter = pipeWriter;
|
_pipeWriter = pipeWriter;
|
||||||
_connectionId = connectionId;
|
_connectionId = connectionId;
|
||||||
|
|
@ -49,6 +66,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
_log = log;
|
_log = log;
|
||||||
_minResponseDataRateFeature = minResponseDataRateFeature;
|
_minResponseDataRateFeature = minResponseDataRateFeature;
|
||||||
_flusher = new TimingPipeFlusher(pipeWriter, timeoutControl, log);
|
_flusher = new TimingPipeFlusher(pipeWriter, timeoutControl, log);
|
||||||
|
_memoryPool = memoryPool;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteDataAsync(ReadOnlySpan<byte> buffer, CancellationToken cancellationToken = default)
|
public Task WriteDataAsync(ReadOnlySpan<byte> buffer, CancellationToken cancellationToken = default)
|
||||||
|
|
@ -58,27 +76,114 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
return Task.FromCanceled(cancellationToken);
|
return Task.FromCanceled(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return WriteAsync(buffer, cancellationToken).AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<FlushResult> WriteDataToPipeAsync(ReadOnlySpan<byte> buffer, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return new ValueTask<FlushResult>(Task.FromCanceled<FlushResult>(cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
return WriteAsync(buffer, cancellationToken);
|
return WriteAsync(buffer, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteStreamSuffixAsync()
|
public ValueTask<FlushResult> WriteStreamSuffixAsync()
|
||||||
{
|
{
|
||||||
return WriteAsync(_endChunkedResponseBytes.Span);
|
return WriteAsync(_endChunkedResponseBytes.Span);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task FlushAsync(CancellationToken cancellationToken = default)
|
public ValueTask<FlushResult> FlushAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return WriteAsync(Constants.EmptyData, cancellationToken);
|
return WriteAsync(Constants.EmptyData, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteChunkAsync(ReadOnlySpan<byte> buffer, CancellationToken cancellationToken)
|
public Memory<byte> GetMemory(int sizeHint = 0)
|
||||||
{
|
{
|
||||||
lock (_contextLock)
|
lock (_contextLock)
|
||||||
{
|
{
|
||||||
if (_completed)
|
if (_completed)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return GetFakeMemory(sizeHint);
|
||||||
}
|
}
|
||||||
|
else if (_autoChunk)
|
||||||
|
{
|
||||||
|
return GetChunkedMemory(sizeHint);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return _pipeWriter.GetMemory(sizeHint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Span<byte> GetSpan(int sizeHint = 0)
|
||||||
|
{
|
||||||
|
lock (_contextLock)
|
||||||
|
{
|
||||||
|
if (_completed)
|
||||||
|
{
|
||||||
|
return GetFakeMemory(sizeHint).Span;
|
||||||
|
}
|
||||||
|
else if (_autoChunk)
|
||||||
|
{
|
||||||
|
return GetChunkedMemory(sizeHint).Span;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return _pipeWriter.GetMemory(sizeHint).Span;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Advance(int bytes)
|
||||||
|
{
|
||||||
|
lock (_contextLock)
|
||||||
|
{
|
||||||
|
if (_completed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_autoChunk)
|
||||||
|
{
|
||||||
|
if (bytes < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes + _advancedBytesForChunk > _currentChunkMemory.Length - BeginChunkLengthMax - EndChunkLength)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Can't advance past buffer size.");
|
||||||
|
}
|
||||||
|
_advancedBytesForChunk += bytes;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_pipeWriter.Advance(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CancelPendingFlush()
|
||||||
|
{
|
||||||
|
_pipeWriter.CancelPendingFlush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method is for chunked http responses that directly call response.WriteAsync
|
||||||
|
public ValueTask<FlushResult> WriteChunkAsync(ReadOnlySpan<byte> buffer, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
lock (_contextLock)
|
||||||
|
{
|
||||||
|
if (_pipeWriterCompleted)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure any memory used with GetMemory/Advance is written before the chunk
|
||||||
|
// passed in.
|
||||||
|
WriteCurrentMemoryToPipeWriter();
|
||||||
|
|
||||||
if (buffer.Length > 0)
|
if (buffer.Length > 0)
|
||||||
{
|
{
|
||||||
|
|
@ -96,11 +201,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
return FlushAsync(cancellationToken);
|
return FlushAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void WriteResponseHeaders(int statusCode, string reasonPhrase, HttpResponseHeaders responseHeaders)
|
public void WriteResponseHeaders(int statusCode, string reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk)
|
||||||
{
|
{
|
||||||
lock (_contextLock)
|
lock (_contextLock)
|
||||||
{
|
{
|
||||||
if (_completed)
|
if (_pipeWriterCompleted)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -117,6 +222,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
writer.Commit();
|
writer.Commit();
|
||||||
|
|
||||||
_unflushedBytes += writer.BytesCommitted;
|
_unflushedBytes += writer.BytesCommitted;
|
||||||
|
_autoChunk = autoChunk;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,12 +230,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
lock (_contextLock)
|
lock (_contextLock)
|
||||||
{
|
{
|
||||||
if (_completed)
|
if (_fakeMemoryOwner != null)
|
||||||
{
|
{
|
||||||
return;
|
_fakeMemoryOwner.Dispose();
|
||||||
|
_fakeMemoryOwner = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CompletePipe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CompletePipe()
|
||||||
|
{
|
||||||
|
if (!_pipeWriterCompleted)
|
||||||
|
{
|
||||||
_log.ConnectionDisconnect(_connectionId);
|
_log.ConnectionDisconnect(_connectionId);
|
||||||
|
_pipeWriterCompleted = true;
|
||||||
_completed = true;
|
_completed = true;
|
||||||
_pipeWriter.Complete();
|
_pipeWriter.Complete();
|
||||||
}
|
}
|
||||||
|
|
@ -139,7 +255,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
// Abort can be called after Dispose if there's a flush timeout.
|
// Abort can be called after Dispose if there's a flush timeout.
|
||||||
// It's important to still call _lifetimeFeature.Abort() in this case.
|
// It's important to still call _lifetimeFeature.Abort() in this case.
|
||||||
|
|
||||||
lock (_contextLock)
|
lock (_contextLock)
|
||||||
{
|
{
|
||||||
if (_aborted)
|
if (_aborted)
|
||||||
|
|
@ -149,24 +264,40 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
|
|
||||||
_aborted = true;
|
_aborted = true;
|
||||||
_connectionContext.Abort(error);
|
_connectionContext.Abort(error);
|
||||||
Dispose();
|
|
||||||
|
CompletePipe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Write100ContinueAsync()
|
public void Complete()
|
||||||
|
{
|
||||||
|
lock (_contextLock)
|
||||||
|
{
|
||||||
|
_completed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<FlushResult> Write100ContinueAsync()
|
||||||
{
|
{
|
||||||
return WriteAsync(_continueBytes.Span);
|
return WriteAsync(_continueBytes.Span);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task WriteAsync(
|
private ValueTask<FlushResult> WriteAsync(
|
||||||
ReadOnlySpan<byte> buffer,
|
ReadOnlySpan<byte> buffer,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
lock (_contextLock)
|
lock (_contextLock)
|
||||||
{
|
{
|
||||||
if (_completed)
|
if (_pipeWriterCompleted)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_autoChunk)
|
||||||
|
{
|
||||||
|
// If there is data that was chunked before writing (ex someone did GetMemory->Advance->WriteAsync)
|
||||||
|
// make sure to write whatever was advanced first
|
||||||
|
WriteCurrentMemoryToPipeWriter();
|
||||||
}
|
}
|
||||||
|
|
||||||
var writer = new BufferWriter<PipeWriter>(_pipeWriter);
|
var writer = new BufferWriter<PipeWriter>(_pipeWriter);
|
||||||
|
|
@ -188,5 +319,77 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// These methods are for chunked http responses that use GetMemory/Advance
|
||||||
|
private Memory<byte> GetChunkedMemory(int sizeHint)
|
||||||
|
{
|
||||||
|
// The max size of a chunk will be the size of memory returned from the PipeWriter (today 4096)
|
||||||
|
// minus 5 for the max chunked prefix size and minus 2 for the chunked ending, leaving a total of
|
||||||
|
// 4089.
|
||||||
|
|
||||||
|
if (!_currentChunkMemoryUpdated)
|
||||||
|
{
|
||||||
|
// First time calling GetMemory
|
||||||
|
_currentChunkMemory = _pipeWriter.GetMemory(sizeHint);
|
||||||
|
_currentChunkMemoryUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var memoryMaxLength = _currentChunkMemory.Length - BeginChunkLengthMax - EndChunkLength;
|
||||||
|
if (_advancedBytesForChunk >= memoryMaxLength - Math.Min(MemorySizeThreshold, sizeHint))
|
||||||
|
{
|
||||||
|
// Chunk is completely written, commit it to the pipe so GetMemory will return a new chunk of memory.
|
||||||
|
WriteCurrentMemoryToPipeWriter();
|
||||||
|
_currentChunkMemory = _pipeWriter.GetMemory(sizeHint);
|
||||||
|
_currentChunkMemoryUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var actualMemory = _currentChunkMemory.Slice(
|
||||||
|
BeginChunkLengthMax + _advancedBytesForChunk,
|
||||||
|
memoryMaxLength - _advancedBytesForChunk);
|
||||||
|
|
||||||
|
Debug.Assert(actualMemory.Length <= 4089);
|
||||||
|
|
||||||
|
return actualMemory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteCurrentMemoryToPipeWriter()
|
||||||
|
{
|
||||||
|
var writer = new BufferWriter<PipeWriter>(_pipeWriter);
|
||||||
|
|
||||||
|
Debug.Assert(_advancedBytesForChunk <= _currentChunkMemory.Length);
|
||||||
|
|
||||||
|
if (_advancedBytesForChunk > 0)
|
||||||
|
{
|
||||||
|
var bytesWritten = writer.WriteBeginChunkBytes(_advancedBytesForChunk);
|
||||||
|
|
||||||
|
Debug.Assert(bytesWritten <= BeginChunkLengthMax);
|
||||||
|
|
||||||
|
if (bytesWritten < BeginChunkLengthMax)
|
||||||
|
{
|
||||||
|
// If the current chunk of memory isn't completely utilized, we need to copy the contents forwards.
|
||||||
|
// This occurs if someone uses less than 255 bytes of the current Memory segment.
|
||||||
|
// Therefore, we need to copy it forwards by either 1 or 2 bytes (depending on number of bytes)
|
||||||
|
_currentChunkMemory.Slice(BeginChunkLengthMax, _advancedBytesForChunk).CopyTo(_currentChunkMemory.Slice(bytesWritten));
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Advance(_advancedBytesForChunk);
|
||||||
|
writer.WriteEndChunkBytes();
|
||||||
|
writer.Commit();
|
||||||
|
_advancedBytesForChunk = 0;
|
||||||
|
_unflushedBytes += writer.BytesCommitted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is an empty write, we still need to update the current chunk
|
||||||
|
_currentChunkMemoryUpdated = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Memory<byte> GetFakeMemory(int sizeHint)
|
||||||
|
{
|
||||||
|
if (_fakeMemoryOwner == null)
|
||||||
|
{
|
||||||
|
_fakeMemoryOwner = _memoryPool.Rent(sizeHint);
|
||||||
|
}
|
||||||
|
return _fakeMemoryOwner.Memory;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.IO.Pipelines;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -10,6 +13,7 @@ using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
|
|
@ -21,7 +25,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
IHttpRequestIdentifierFeature,
|
IHttpRequestIdentifierFeature,
|
||||||
IHttpBodyControlFeature,
|
IHttpBodyControlFeature,
|
||||||
IHttpMaxRequestBodySizeFeature,
|
IHttpMaxRequestBodySizeFeature,
|
||||||
IHttpResponseStartFeature
|
IHttpResponseStartFeature,
|
||||||
|
IResponseBodyPipeFeature
|
||||||
{
|
{
|
||||||
// NOTE: When feature interfaces are added to or removed from this HttpProtocol class implementation,
|
// NOTE: When feature interfaces are added to or removed from this HttpProtocol class implementation,
|
||||||
// then the list of `implementedFeatures` in the generated code project MUST also be updated.
|
// then the list of `implementedFeatures` in the generated code project MUST also be updated.
|
||||||
|
|
@ -111,12 +116,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
set => ResponseHeaders = value;
|
set => ResponseHeaders = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream IHttpResponseFeature.Body
|
|
||||||
{
|
|
||||||
get => ResponseBody;
|
|
||||||
set => ResponseBody = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
CancellationToken IHttpRequestLifetimeFeature.RequestAborted
|
CancellationToken IHttpRequestLifetimeFeature.RequestAborted
|
||||||
{
|
{
|
||||||
get => RequestAborted;
|
get => RequestAborted;
|
||||||
|
|
@ -193,6 +192,40 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PipeWriter IResponseBodyPipeFeature.ResponseBodyPipe
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return ResponsePipeWriter;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
ResponsePipeWriter = value;
|
||||||
|
ResponseBody = new WriteOnlyPipeStream(ResponsePipeWriter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream IHttpResponseFeature.Body
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return ResponseBody;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
ResponseBody = value;
|
||||||
|
var responsePipeWriter = new StreamPipeWriter(ResponseBody, minimumSegmentSize: KestrelMemoryPool.MinimumSegmentSize, _context.MemoryPool);
|
||||||
|
ResponsePipeWriter = responsePipeWriter;
|
||||||
|
|
||||||
|
// The StreamPipeWrapper needs to be disposed as it hold onto blocks of memory
|
||||||
|
if (_wrapperObjectsToDispose == null)
|
||||||
|
{
|
||||||
|
_wrapperObjectsToDispose = new List<IDisposable>();
|
||||||
|
}
|
||||||
|
_wrapperObjectsToDispose.Add(responsePipeWriter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void ResetHttp1Features()
|
protected void ResetHttp1Features()
|
||||||
{
|
{
|
||||||
_currentIHttpMinRequestBodyDataRateFeature = this;
|
_currentIHttpMinRequestBodyDataRateFeature = this;
|
||||||
|
|
@ -250,7 +283,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
ApplicationAbort();
|
ApplicationAbort();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void ApplicationAbort();
|
|
||||||
|
|
||||||
Task IHttpResponseStartFeature.StartAsync(CancellationToken cancellationToken)
|
Task IHttpResponseStartFeature.StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|
@ -259,6 +291,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
return InitializeResponseAsync(0);
|
return InitializeResponseAsync(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
private static readonly Type IHttpRequestFeatureType = typeof(IHttpRequestFeature);
|
private static readonly Type IHttpRequestFeatureType = typeof(IHttpRequestFeature);
|
||||||
private static readonly Type IHttpResponseFeatureType = typeof(IHttpResponseFeature);
|
private static readonly Type IHttpResponseFeatureType = typeof(IHttpResponseFeature);
|
||||||
|
private static readonly Type IResponseBodyPipeFeatureType = typeof(IResponseBodyPipeFeature);
|
||||||
private static readonly Type IHttpRequestIdentifierFeatureType = typeof(IHttpRequestIdentifierFeature);
|
private static readonly Type IHttpRequestIdentifierFeatureType = typeof(IHttpRequestIdentifierFeature);
|
||||||
private static readonly Type IServiceProvidersFeatureType = typeof(IServiceProvidersFeature);
|
private static readonly Type IServiceProvidersFeatureType = typeof(IServiceProvidersFeature);
|
||||||
private static readonly Type IHttpRequestLifetimeFeatureType = typeof(IHttpRequestLifetimeFeature);
|
private static readonly Type IHttpRequestLifetimeFeatureType = typeof(IHttpRequestLifetimeFeature);
|
||||||
|
|
@ -39,6 +40,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
|
|
||||||
private object _currentIHttpRequestFeature;
|
private object _currentIHttpRequestFeature;
|
||||||
private object _currentIHttpResponseFeature;
|
private object _currentIHttpResponseFeature;
|
||||||
|
private object _currentIResponseBodyPipeFeature;
|
||||||
private object _currentIHttpRequestIdentifierFeature;
|
private object _currentIHttpRequestIdentifierFeature;
|
||||||
private object _currentIServiceProvidersFeature;
|
private object _currentIServiceProvidersFeature;
|
||||||
private object _currentIHttpRequestLifetimeFeature;
|
private object _currentIHttpRequestLifetimeFeature;
|
||||||
|
|
@ -69,6 +71,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
_currentIHttpRequestFeature = this;
|
_currentIHttpRequestFeature = this;
|
||||||
_currentIHttpResponseFeature = this;
|
_currentIHttpResponseFeature = this;
|
||||||
|
_currentIResponseBodyPipeFeature = this;
|
||||||
_currentIHttpUpgradeFeature = this;
|
_currentIHttpUpgradeFeature = this;
|
||||||
_currentIHttpRequestIdentifierFeature = this;
|
_currentIHttpRequestIdentifierFeature = this;
|
||||||
_currentIHttpRequestLifetimeFeature = this;
|
_currentIHttpRequestLifetimeFeature = this;
|
||||||
|
|
@ -153,6 +156,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
feature = _currentIHttpResponseFeature;
|
feature = _currentIHttpResponseFeature;
|
||||||
}
|
}
|
||||||
|
else if (key == IResponseBodyPipeFeatureType)
|
||||||
|
{
|
||||||
|
feature = _currentIResponseBodyPipeFeature;
|
||||||
|
}
|
||||||
else if (key == IHttpRequestIdentifierFeatureType)
|
else if (key == IHttpRequestIdentifierFeatureType)
|
||||||
{
|
{
|
||||||
feature = _currentIHttpRequestIdentifierFeature;
|
feature = _currentIHttpRequestIdentifierFeature;
|
||||||
|
|
@ -257,6 +264,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
_currentIHttpResponseFeature = value;
|
_currentIHttpResponseFeature = value;
|
||||||
}
|
}
|
||||||
|
else if (key == IResponseBodyPipeFeatureType)
|
||||||
|
{
|
||||||
|
_currentIResponseBodyPipeFeature = value;
|
||||||
|
}
|
||||||
else if (key == IHttpRequestIdentifierFeatureType)
|
else if (key == IHttpRequestIdentifierFeatureType)
|
||||||
{
|
{
|
||||||
_currentIHttpRequestIdentifierFeature = value;
|
_currentIHttpRequestIdentifierFeature = value;
|
||||||
|
|
@ -359,6 +370,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
feature = (TFeature)_currentIHttpResponseFeature;
|
feature = (TFeature)_currentIHttpResponseFeature;
|
||||||
}
|
}
|
||||||
|
else if (typeof(TFeature) == typeof(IResponseBodyPipeFeature))
|
||||||
|
{
|
||||||
|
feature = (TFeature)_currentIResponseBodyPipeFeature;
|
||||||
|
}
|
||||||
else if (typeof(TFeature) == typeof(IHttpRequestIdentifierFeature))
|
else if (typeof(TFeature) == typeof(IHttpRequestIdentifierFeature))
|
||||||
{
|
{
|
||||||
feature = (TFeature)_currentIHttpRequestIdentifierFeature;
|
feature = (TFeature)_currentIHttpRequestIdentifierFeature;
|
||||||
|
|
@ -467,6 +482,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
_currentIHttpResponseFeature = feature;
|
_currentIHttpResponseFeature = feature;
|
||||||
}
|
}
|
||||||
|
else if (typeof(TFeature) == typeof(IResponseBodyPipeFeature))
|
||||||
|
{
|
||||||
|
_currentIResponseBodyPipeFeature = feature;
|
||||||
|
}
|
||||||
else if (typeof(TFeature) == typeof(IHttpRequestIdentifierFeature))
|
else if (typeof(TFeature) == typeof(IHttpRequestIdentifierFeature))
|
||||||
{
|
{
|
||||||
_currentIHttpRequestIdentifierFeature = feature;
|
_currentIHttpRequestIdentifierFeature = feature;
|
||||||
|
|
@ -567,6 +586,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
yield return new KeyValuePair<Type, object>(IHttpResponseFeatureType, _currentIHttpResponseFeature);
|
yield return new KeyValuePair<Type, object>(IHttpResponseFeatureType, _currentIHttpResponseFeature);
|
||||||
}
|
}
|
||||||
|
if (_currentIResponseBodyPipeFeature != null)
|
||||||
|
{
|
||||||
|
yield return new KeyValuePair<Type, object>(IResponseBodyPipeFeatureType, _currentIResponseBodyPipeFeature);
|
||||||
|
}
|
||||||
if (_currentIHttpRequestIdentifierFeature != null)
|
if (_currentIHttpRequestIdentifierFeature != null)
|
||||||
{
|
{
|
||||||
yield return new KeyValuePair<Type, object>(IHttpRequestIdentifierFeatureType, _currentIHttpRequestIdentifierFeature);
|
yield return new KeyValuePair<Type, object>(IHttpRequestIdentifierFeatureType, _currentIHttpRequestIdentifierFeature);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
@ -31,7 +30,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: " + Constants.ServerName);
|
private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: " + Constants.ServerName);
|
||||||
|
|
||||||
protected Streams _streams;
|
protected Streams _streams;
|
||||||
|
private HttpResponsePipeWriter _originalPipeWriter;
|
||||||
private Stack<KeyValuePair<Func<object, Task>, object>> _onStarting;
|
private Stack<KeyValuePair<Func<object, Task>, object>> _onStarting;
|
||||||
private Stack<KeyValuePair<Func<object, Task>, object>> _onCompleted;
|
private Stack<KeyValuePair<Func<object, Task>, object>> _onCompleted;
|
||||||
|
|
||||||
|
|
@ -64,6 +63,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
protected string _methodText = null;
|
protected string _methodText = null;
|
||||||
private string _scheme = null;
|
private string _scheme = null;
|
||||||
|
|
||||||
|
private List<IDisposable> _wrapperObjectsToDispose;
|
||||||
|
|
||||||
public HttpProtocol(HttpConnectionContext context)
|
public HttpProtocol(HttpConnectionContext context)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
|
@ -227,6 +228,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
|
|
||||||
public IHeaderDictionary ResponseHeaders { get; set; }
|
public IHeaderDictionary ResponseHeaders { get; set; }
|
||||||
public Stream ResponseBody { get; set; }
|
public Stream ResponseBody { get; set; }
|
||||||
|
public PipeWriter ResponsePipeWriter { get; set; }
|
||||||
|
|
||||||
public CancellationToken RequestAborted
|
public CancellationToken RequestAborted
|
||||||
{
|
{
|
||||||
|
|
@ -295,10 +297,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
if (_streams == null)
|
if (_streams == null)
|
||||||
{
|
{
|
||||||
_streams = new Streams(bodyControl: this, httpResponseControl: this);
|
var pipeWriter = new HttpResponsePipeWriter(this);
|
||||||
|
_streams = new Streams(bodyControl: this, pipeWriter);
|
||||||
|
_originalPipeWriter = pipeWriter;
|
||||||
}
|
}
|
||||||
|
|
||||||
(RequestBody, ResponseBody) = _streams.Start(messageBody);
|
(RequestBody, ResponseBody) = _streams.Start(messageBody);
|
||||||
|
ResponsePipeWriter = _originalPipeWriter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void StopStreams() => _streams.Stop();
|
public void StopStreams() => _streams.Stop();
|
||||||
|
|
@ -371,6 +376,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_wrapperObjectsToDispose != null)
|
||||||
|
{
|
||||||
|
foreach (var disposable in _wrapperObjectsToDispose)
|
||||||
|
{
|
||||||
|
disposable.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_requestHeadersParsed = 0;
|
_requestHeadersParsed = 0;
|
||||||
|
|
||||||
_responseBytesWritten = 0;
|
_responseBytesWritten = 0;
|
||||||
|
|
@ -382,6 +395,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
|
|
||||||
protected abstract void OnReset();
|
protected abstract void OnReset();
|
||||||
|
|
||||||
|
protected abstract void ApplicationAbort();
|
||||||
|
|
||||||
protected virtual void OnRequestProcessingEnding()
|
protected virtual void OnRequestProcessingEnding()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
@ -780,21 +795,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
|
|
||||||
{
|
|
||||||
if (!HasResponseStarted)
|
|
||||||
{
|
|
||||||
var initializeTask = InitializeResponseAsync(0);
|
|
||||||
// If return is Task.CompletedTask no awaiting is required
|
|
||||||
if (!ReferenceEquals(initializeTask, Task.CompletedTask))
|
|
||||||
{
|
|
||||||
return InitializeAndFlushAsyncAwaited(initializeTask, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return FlushAsyncInternal(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
private async Task InitializeAndFlushAsyncAwaited(Task initializeTask, CancellationToken cancellationToken)
|
private async Task InitializeAndFlushAsyncAwaited(Task initializeTask, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|
@ -803,85 +803,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
private Task FlushAsyncInternal(CancellationToken cancellationToken)
|
private ValueTask<FlushResult> FlushAsyncInternal(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_requestProcessingStatus = RequestProcessingStatus.HeadersFlushed;
|
_requestProcessingStatus = RequestProcessingStatus.HeadersFlushed;
|
||||||
return Output.FlushAsync(cancellationToken);
|
return Output.FlushAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default(CancellationToken))
|
|
||||||
{
|
|
||||||
// For the first write, ensure headers are flushed if WriteDataAsync isn't called.
|
|
||||||
var firstWrite = !HasResponseStarted;
|
|
||||||
|
|
||||||
if (firstWrite)
|
|
||||||
{
|
|
||||||
var initializeTask = InitializeResponseAsync(data.Length);
|
|
||||||
// If return is Task.CompletedTask no awaiting is required
|
|
||||||
if (!ReferenceEquals(initializeTask, Task.CompletedTask))
|
|
||||||
{
|
|
||||||
return WriteAsyncAwaited(initializeTask, data, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
VerifyAndUpdateWrite(data.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_canWriteResponseBody)
|
|
||||||
{
|
|
||||||
if (_autoChunk)
|
|
||||||
{
|
|
||||||
if (data.Length == 0)
|
|
||||||
{
|
|
||||||
return !firstWrite ? Task.CompletedTask : FlushAsyncInternal(cancellationToken);
|
|
||||||
}
|
|
||||||
return WriteChunkedAsync(data.Span, cancellationToken);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CheckLastWrite();
|
|
||||||
return WriteDataAsync(data, cancellationToken: cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
HandleNonBodyResponseWrite();
|
|
||||||
return !firstWrite ? Task.CompletedTask : FlushAsyncInternal(cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task WriteAsyncAwaited(Task initializeTask, ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
await initializeTask;
|
|
||||||
|
|
||||||
// WriteAsyncAwaited is only called for the first write to the body.
|
|
||||||
// Ensure headers are flushed if Write(Chunked)Async isn't called.
|
|
||||||
if (_canWriteResponseBody)
|
|
||||||
{
|
|
||||||
if (_autoChunk)
|
|
||||||
{
|
|
||||||
if (data.Length == 0)
|
|
||||||
{
|
|
||||||
await FlushAsyncInternal(cancellationToken);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await WriteChunkedAsync(data.Span, cancellationToken);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CheckLastWrite();
|
|
||||||
await WriteDataAsync(data, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
HandleNonBodyResponseWrite();
|
|
||||||
await FlushAsyncInternal(cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task WriteDataAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
|
private Task WriteDataAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_requestProcessingStatus = RequestProcessingStatus.HeadersFlushed;
|
_requestProcessingStatus = RequestProcessingStatus.HeadersFlushed;
|
||||||
|
|
@ -958,12 +885,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task WriteChunkedAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_requestProcessingStatus = RequestProcessingStatus.HeadersFlushed;
|
|
||||||
return Output.WriteChunkAsync(data, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ProduceContinue()
|
public void ProduceContinue()
|
||||||
{
|
{
|
||||||
if (HasResponseStarted)
|
if (HasResponseStarted)
|
||||||
|
|
@ -1090,7 +1011,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
|
|
||||||
if (!HasFlushedHeaders)
|
if (!HasFlushedHeaders)
|
||||||
{
|
{
|
||||||
return FlushAsyncInternal(default);
|
return FlushAsyncInternal(default).AsTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
@ -1208,7 +1129,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
responseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes);
|
responseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
Output.WriteResponseHeaders(StatusCode, ReasonPhrase, responseHeaders);
|
Output.WriteResponseHeaders(StatusCode, ReasonPhrase, responseHeaders, _autoChunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanWriteResponseBody()
|
private bool CanWriteResponseBody()
|
||||||
|
|
@ -1345,5 +1266,172 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
|
|
||||||
Log.ApplicationError(ConnectionId, TraceIdentifier, ex);
|
Log.ApplicationError(ConnectionId, TraceIdentifier, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Advance(int bytes)
|
||||||
|
{
|
||||||
|
if (_canWriteResponseBody)
|
||||||
|
{
|
||||||
|
VerifyAndUpdateWrite(bytes);
|
||||||
|
Output.Advance(bytes);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HandleNonBodyResponseWrite();
|
||||||
|
|
||||||
|
// For HEAD requests, we still use the number of bytes written for logging
|
||||||
|
// how many bytes were written.
|
||||||
|
VerifyAndUpdateWrite(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Memory<byte> GetMemory(int sizeHint = 0)
|
||||||
|
{
|
||||||
|
ThrowIfResponseNotStarted();
|
||||||
|
|
||||||
|
return Output.GetMemory(sizeHint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Span<byte> GetSpan(int sizeHint = 0)
|
||||||
|
{
|
||||||
|
ThrowIfResponseNotStarted();
|
||||||
|
|
||||||
|
return Output.GetSpan(sizeHint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[StackTraceHidden]
|
||||||
|
private void ThrowIfResponseNotStarted()
|
||||||
|
{
|
||||||
|
if (!HasResponseStarted)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(CoreStrings.StartAsyncBeforeGetMemory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<FlushResult> FlushPipeAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!HasResponseStarted)
|
||||||
|
{
|
||||||
|
var initializeTask = InitializeResponseAsync(0);
|
||||||
|
// If return is Task.CompletedTask no awaiting is required
|
||||||
|
if (!ReferenceEquals(initializeTask, Task.CompletedTask))
|
||||||
|
{
|
||||||
|
return FlushAsyncAwaited(initializeTask, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Output.FlushAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CancelPendingFlush()
|
||||||
|
{
|
||||||
|
Output.CancelPendingFlush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Complete(Exception ex)
|
||||||
|
{
|
||||||
|
if (ex != null)
|
||||||
|
{
|
||||||
|
var wrappedException = new ConnectionAbortedException("The BodyPipe was completed with an exception.", ex);
|
||||||
|
ReportApplicationError(wrappedException);
|
||||||
|
|
||||||
|
if (HasResponseStarted)
|
||||||
|
{
|
||||||
|
ApplicationAbort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Output.Complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<FlushResult> WritePipeAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// For the first write, ensure headers are flushed if WriteDataAsync isn't called.
|
||||||
|
var firstWrite = !HasResponseStarted;
|
||||||
|
|
||||||
|
if (firstWrite)
|
||||||
|
{
|
||||||
|
var initializeTask = InitializeResponseAsync(data.Length);
|
||||||
|
// If return is Task.CompletedTask no awaiting is required
|
||||||
|
if (!ReferenceEquals(initializeTask, Task.CompletedTask))
|
||||||
|
{
|
||||||
|
return WriteAsyncAwaited(initializeTask, data, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
VerifyAndUpdateWrite(data.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_canWriteResponseBody)
|
||||||
|
{
|
||||||
|
if (_autoChunk)
|
||||||
|
{
|
||||||
|
if (data.Length == 0)
|
||||||
|
{
|
||||||
|
return !firstWrite ? default : FlushAsyncInternal(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
_requestProcessingStatus = RequestProcessingStatus.HeadersFlushed;
|
||||||
|
return Output.WriteChunkAsync(data.Span, cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CheckLastWrite();
|
||||||
|
return Output.WriteDataToPipeAsync(data.Span, cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HandleNonBodyResponseWrite();
|
||||||
|
return !firstWrite ? default : FlushAsyncInternal(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return FlushPipeAsync(cancellationToken).AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
private async ValueTask<FlushResult> FlushAsyncAwaited(Task initializeTask, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await initializeTask;
|
||||||
|
return await Output.FlushAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task WriteAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return WritePipeAsync(data, cancellationToken).AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<FlushResult> WriteAsyncAwaited(Task initializeTask, ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await initializeTask;
|
||||||
|
|
||||||
|
// WriteAsyncAwaited is only called for the first write to the body.
|
||||||
|
// Ensure headers are flushed if Write(Chunked)Async isn't called.
|
||||||
|
if (_canWriteResponseBody)
|
||||||
|
{
|
||||||
|
if (_autoChunk)
|
||||||
|
{
|
||||||
|
if (data.Length == 0)
|
||||||
|
{
|
||||||
|
return await FlushAsyncInternal(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
_requestProcessingStatus = RequestProcessingStatus.HeadersFlushed;
|
||||||
|
return await Output.WriteChunkAsync(data.Span, cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CheckLastWrite();
|
||||||
|
return await Output.WriteDataToPipeAsync(data.Span, cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HandleNonBodyResponseWrite();
|
||||||
|
return await FlushAsyncInternal(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
|
{
|
||||||
|
public class HttpResponsePipeWriter : PipeWriter
|
||||||
|
{
|
||||||
|
private HttpStreamState _state;
|
||||||
|
private readonly IHttpResponseControl _pipeControl;
|
||||||
|
|
||||||
|
public HttpResponsePipeWriter(IHttpResponseControl pipeControl)
|
||||||
|
{
|
||||||
|
_pipeControl = pipeControl;
|
||||||
|
_state = HttpStreamState.Closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Advance(int bytes)
|
||||||
|
{
|
||||||
|
ValidateState();
|
||||||
|
_pipeControl.Advance(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void CancelPendingFlush()
|
||||||
|
{
|
||||||
|
ValidateState();
|
||||||
|
_pipeControl.CancelPendingFlush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Complete(Exception exception = null)
|
||||||
|
{
|
||||||
|
_pipeControl.Complete(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ValueTask<FlushResult> FlushAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ValidateState(cancellationToken);
|
||||||
|
return _pipeControl.FlushPipeAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Memory<byte> GetMemory(int sizeHint = 0)
|
||||||
|
{
|
||||||
|
ValidateState();
|
||||||
|
return _pipeControl.GetMemory(sizeHint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Span<byte> GetSpan(int sizeHint = 0)
|
||||||
|
{
|
||||||
|
ValidateState();
|
||||||
|
return _pipeControl.GetSpan(sizeHint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnReaderCompleted(Action<Exception, object> callback, object state)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ValueTask<FlushResult> WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ValidateState(cancellationToken);
|
||||||
|
return _pipeControl.WritePipeAsync(source, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartAcceptingWrites()
|
||||||
|
{
|
||||||
|
// Only start if not aborted
|
||||||
|
if (_state == HttpStreamState.Closed)
|
||||||
|
{
|
||||||
|
_state = HttpStreamState.Open;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopAcceptingWrites()
|
||||||
|
{
|
||||||
|
// Can't use dispose (or close) as can be disposed too early by user code
|
||||||
|
// As exampled in EngineTests.ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes
|
||||||
|
_state = HttpStreamState.Closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Abort()
|
||||||
|
{
|
||||||
|
// We don't want to throw an ODE until the app func actually completes.
|
||||||
|
if (_state != HttpStreamState.Closed)
|
||||||
|
{
|
||||||
|
_state = HttpStreamState.Aborted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private void ValidateState(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var state = _state;
|
||||||
|
if (state == HttpStreamState.Open || state == HttpStreamState.Aborted)
|
||||||
|
{
|
||||||
|
// Aborted state only throws on write if cancellationToken requests it
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ThrowObjectDisposedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThrowObjectDisposedException()
|
||||||
|
{
|
||||||
|
throw new ObjectDisposedException(nameof(HttpResponseStream), CoreStrings.WritingToResponseBodyAfterResponseCompleted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,59 +2,21 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO.Pipelines;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
internal class HttpResponseStream : WriteOnlyStream
|
internal class HttpResponseStream : WriteOnlyPipeStream
|
||||||
{
|
{
|
||||||
|
private readonly HttpResponsePipeWriter _pipeWriter;
|
||||||
private readonly IHttpBodyControlFeature _bodyControl;
|
private readonly IHttpBodyControlFeature _bodyControl;
|
||||||
private readonly IHttpResponseControl _httpResponseControl;
|
|
||||||
private HttpStreamState _state;
|
|
||||||
|
|
||||||
public HttpResponseStream(IHttpBodyControlFeature bodyControl, IHttpResponseControl httpResponseControl)
|
public HttpResponseStream(IHttpBodyControlFeature bodyControl, HttpResponsePipeWriter pipeWriter)
|
||||||
|
: base(pipeWriter)
|
||||||
{
|
{
|
||||||
_bodyControl = bodyControl;
|
_bodyControl = bodyControl;
|
||||||
_httpResponseControl = httpResponseControl;
|
_pipeWriter = pipeWriter;
|
||||||
_state = HttpStreamState.Closed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool CanSeek => false;
|
|
||||||
|
|
||||||
public override long Length
|
|
||||||
=> throw new NotSupportedException();
|
|
||||||
|
|
||||||
public override long Position
|
|
||||||
{
|
|
||||||
get => throw new NotSupportedException();
|
|
||||||
set => throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Flush()
|
|
||||||
{
|
|
||||||
FlushAsync(default).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Task FlushAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
ValidateState(cancellationToken);
|
|
||||||
|
|
||||||
return _httpResponseControl.FlushAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override long Seek(long offset, SeekOrigin origin)
|
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void SetLength(long value)
|
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Write(byte[] buffer, int offset, int count)
|
public override void Write(byte[] buffer, int offset, int count)
|
||||||
|
|
@ -64,104 +26,32 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
throw new InvalidOperationException(CoreStrings.SynchronousWritesDisallowed);
|
throw new InvalidOperationException(CoreStrings.SynchronousWritesDisallowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteAsync(buffer, offset, count, default).GetAwaiter().GetResult();
|
base.Write(buffer, offset, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
public override void Flush()
|
||||||
{
|
{
|
||||||
var task = WriteAsync(buffer, offset, count, default, state);
|
if (!_bodyControl.AllowSynchronousIO)
|
||||||
if (callback != null)
|
|
||||||
{
|
{
|
||||||
task.ContinueWith(t => callback.Invoke(t));
|
throw new InvalidOperationException(CoreStrings.SynchronousWritesDisallowed);
|
||||||
}
|
}
|
||||||
return task;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void EndWrite(IAsyncResult asyncResult)
|
base.Flush();
|
||||||
{
|
|
||||||
((Task<object>)asyncResult).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state)
|
|
||||||
{
|
|
||||||
var tcs = new TaskCompletionSource<object>(state);
|
|
||||||
var task = WriteAsync(buffer, offset, count, cancellationToken);
|
|
||||||
task.ContinueWith((task2, state2) =>
|
|
||||||
{
|
|
||||||
var tcs2 = (TaskCompletionSource<object>)state2;
|
|
||||||
if (task2.IsCanceled)
|
|
||||||
{
|
|
||||||
tcs2.SetCanceled();
|
|
||||||
}
|
|
||||||
else if (task2.IsFaulted)
|
|
||||||
{
|
|
||||||
tcs2.SetException(task2.Exception);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
tcs2.SetResult(null);
|
|
||||||
}
|
|
||||||
}, tcs, cancellationToken);
|
|
||||||
return tcs.Task;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
ValidateState(cancellationToken);
|
|
||||||
|
|
||||||
return _httpResponseControl.WriteAsync(new ReadOnlyMemory<byte>(buffer, offset, count), cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ValidateState(cancellationToken);
|
|
||||||
|
|
||||||
return new ValueTask(_httpResponseControl.WriteAsync(source, cancellationToken));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void StartAcceptingWrites()
|
public void StartAcceptingWrites()
|
||||||
{
|
{
|
||||||
// Only start if not aborted
|
_pipeWriter.StartAcceptingWrites();
|
||||||
if (_state == HttpStreamState.Closed)
|
|
||||||
{
|
|
||||||
_state = HttpStreamState.Open;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void StopAcceptingWrites()
|
public void StopAcceptingWrites()
|
||||||
{
|
{
|
||||||
// Can't use dispose (or close) as can be disposed too early by user code
|
_pipeWriter.StopAcceptingWrites();
|
||||||
// As exampled in EngineTests.ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes
|
|
||||||
_state = HttpStreamState.Closed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Abort()
|
public void Abort()
|
||||||
{
|
{
|
||||||
// We don't want to throw an ODE until the app func actually completes.
|
_pipeWriter.Abort();
|
||||||
if (_state != HttpStreamState.Closed)
|
|
||||||
{
|
|
||||||
_state = HttpStreamState.Aborted;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private void ValidateState(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var state = _state;
|
|
||||||
if (state == HttpStreamState.Open || state == HttpStreamState.Aborted)
|
|
||||||
{
|
|
||||||
// Aborted state only throws on write if cancellationToken requests it
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ThrowObjectDisposedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ThrowObjectDisposedException()
|
|
||||||
{
|
|
||||||
throw new ObjectDisposedException(nameof(HttpResponseStream), CoreStrings.WritingToResponseBodyAfterResponseCompleted);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Connections;
|
using Microsoft.AspNetCore.Connections;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.IO.Pipelines;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
|
@ -9,12 +10,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
public interface IHttpOutputProducer
|
public interface IHttpOutputProducer
|
||||||
{
|
{
|
||||||
Task WriteChunkAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken);
|
ValueTask<FlushResult> WriteChunkAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken);
|
||||||
Task FlushAsync(CancellationToken cancellationToken);
|
ValueTask<FlushResult> FlushAsync(CancellationToken cancellationToken);
|
||||||
Task Write100ContinueAsync();
|
ValueTask<FlushResult> Write100ContinueAsync();
|
||||||
void WriteResponseHeaders(int statusCode, string ReasonPhrase, HttpResponseHeaders responseHeaders);
|
void WriteResponseHeaders(int statusCode, string ReasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk);
|
||||||
// This takes ReadOnlySpan instead of ReadOnlyMemory because it always synchronously copies data before flushing.
|
// This takes ReadOnlySpan instead of ReadOnlyMemory because it always synchronously copies data before flushing.
|
||||||
|
ValueTask<FlushResult> WriteDataToPipeAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken);
|
||||||
Task WriteDataAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken);
|
Task WriteDataAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken);
|
||||||
Task WriteStreamSuffixAsync();
|
ValueTask<FlushResult> WriteStreamSuffixAsync();
|
||||||
|
void Advance(int bytes);
|
||||||
|
Span<byte> GetSpan(int sizeHint = 0);
|
||||||
|
Memory<byte> GetMemory(int sizeHint = 0);
|
||||||
|
void CancelPendingFlush();
|
||||||
|
void Complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.IO.Pipelines;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
|
@ -10,7 +11,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
public interface IHttpResponseControl
|
public interface IHttpResponseControl
|
||||||
{
|
{
|
||||||
void ProduceContinue();
|
void ProduceContinue();
|
||||||
Task WriteAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken);
|
Memory<byte> GetMemory(int sizeHint = 0);
|
||||||
Task FlushAsync(CancellationToken cancellationToken);
|
Span<byte> GetSpan(int sizeHint = 0);
|
||||||
|
void Advance(int bytes);
|
||||||
|
ValueTask<FlushResult> FlushPipeAsync(CancellationToken cancellationToken);
|
||||||
|
ValueTask<FlushResult> WritePipeAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken);
|
||||||
|
void CancelPendingFlush();
|
||||||
|
void Complete(Exception exception = null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
|
{
|
||||||
|
public interface IHttpResponsePipeWriterControl
|
||||||
|
{
|
||||||
|
void ProduceContinue();
|
||||||
|
Memory<byte> GetMemory(int sizeHint = 0);
|
||||||
|
Span<byte> GetSpan(int sizeHint = 0);
|
||||||
|
void Advance(int bytes);
|
||||||
|
ValueTask<FlushResult> FlushPipeAsync(CancellationToken cancellationToken);
|
||||||
|
ValueTask<FlushResult> WritePipeAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken);
|
||||||
|
void CancelPendingFlush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -705,7 +705,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ackTask;
|
return ackTask.AsTask();
|
||||||
}
|
}
|
||||||
catch (Http2SettingsParameterOutOfRangeException ex)
|
catch (Http2SettingsParameterOutOfRangeException ex)
|
||||||
{
|
{
|
||||||
|
|
@ -738,7 +738,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _frameWriter.WritePingAsync(Http2PingFrameFlags.ACK, payload);
|
return _frameWriter.WritePingAsync(Http2PingFrameFlags.ACK, payload).AsTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task ProcessGoAwayFrameAsync()
|
private Task ProcessGoAwayFrameAsync()
|
||||||
|
|
|
||||||
|
|
@ -109,13 +109,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task FlushAsync(IHttpOutputAborter outputAborter, CancellationToken cancellationToken)
|
public ValueTask<FlushResult> FlushAsync(IHttpOutputAborter outputAborter, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
lock (_writeLock)
|
lock (_writeLock)
|
||||||
{
|
{
|
||||||
if (_completed)
|
if (_completed)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
var bytesWritten = _unflushedBytes;
|
var bytesWritten = _unflushedBytes;
|
||||||
|
|
@ -125,13 +125,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Write100ContinueAsync(int streamId)
|
public ValueTask<FlushResult> Write100ContinueAsync(int streamId)
|
||||||
{
|
{
|
||||||
lock (_writeLock)
|
lock (_writeLock)
|
||||||
{
|
{
|
||||||
if (_completed)
|
if (_completed)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
_outgoingFrame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS, streamId);
|
_outgoingFrame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS, streamId);
|
||||||
|
|
@ -181,13 +181,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteResponseTrailers(int streamId, HttpResponseTrailers headers)
|
public ValueTask<FlushResult> WriteResponseTrailers(int streamId, HttpResponseTrailers headers)
|
||||||
{
|
{
|
||||||
lock (_writeLock)
|
lock (_writeLock)
|
||||||
{
|
{
|
||||||
if (_completed)
|
if (_completed)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
@ -236,7 +236,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteDataAsync(int streamId, StreamOutputFlowControl flowControl, ReadOnlySequence<byte> data, bool endStream)
|
public ValueTask<FlushResult> WriteDataAsync(int streamId, StreamOutputFlowControl flowControl, ReadOnlySequence<byte> data, bool endStream)
|
||||||
{
|
{
|
||||||
// The Length property of a ReadOnlySequence can be expensive, so we cache the value.
|
// The Length property of a ReadOnlySequence can be expensive, so we cache the value.
|
||||||
var dataLength = data.Length;
|
var dataLength = data.Length;
|
||||||
|
|
@ -245,7 +245,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
{
|
{
|
||||||
if (_completed || flowControl.IsAborted)
|
if (_completed || flowControl.IsAborted)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zero-length data frames are allowed to be sent immediately even if there is no space available in the flow control window.
|
// Zero-length data frames are allowed to be sent immediately even if there is no space available in the flow control window.
|
||||||
|
|
@ -312,12 +312,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
// Plus padding
|
// Plus padding
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task WriteDataAsync(int streamId, StreamOutputFlowControl flowControl, ReadOnlySequence<byte> data, long dataLength, bool endStream)
|
private async ValueTask<FlushResult> WriteDataAsync(int streamId, StreamOutputFlowControl flowControl, ReadOnlySequence<byte> data, long dataLength, bool endStream)
|
||||||
{
|
{
|
||||||
|
FlushResult flushResult = default;
|
||||||
|
|
||||||
while (dataLength > 0)
|
while (dataLength > 0)
|
||||||
{
|
{
|
||||||
OutputFlowControlAwaitable availabilityAwaitable;
|
OutputFlowControlAwaitable availabilityAwaitable;
|
||||||
var writeTask = Task.CompletedTask;
|
var writeTask = default(ValueTask<FlushResult>);
|
||||||
|
|
||||||
lock (_writeLock)
|
lock (_writeLock)
|
||||||
{
|
{
|
||||||
|
|
@ -373,7 +375,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
await availabilityAwaitable;
|
await availabilityAwaitable;
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeTask;
|
flushResult = await writeTask;
|
||||||
|
|
||||||
if (_minResponseDataRate != null)
|
if (_minResponseDataRate != null)
|
||||||
{
|
{
|
||||||
|
|
@ -383,6 +385,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
|
|
||||||
// Ensure that the application continuation isn't executed inline by ProcessWindowUpdateFrameAsync.
|
// Ensure that the application continuation isn't executed inline by ProcessWindowUpdateFrameAsync.
|
||||||
await ThreadPoolAwaitable.Instance;
|
await ThreadPoolAwaitable.Instance;
|
||||||
|
|
||||||
|
return flushResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* https://tools.ietf.org/html/rfc7540#section-6.9
|
/* https://tools.ietf.org/html/rfc7540#section-6.9
|
||||||
|
|
@ -390,13 +394,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
|R| Window Size Increment (31) |
|
|R| Window Size Increment (31) |
|
||||||
+-+-------------------------------------------------------------+
|
+-+-------------------------------------------------------------+
|
||||||
*/
|
*/
|
||||||
public Task WriteWindowUpdateAsync(int streamId, int sizeIncrement)
|
public ValueTask<FlushResult> WriteWindowUpdateAsync(int streamId, int sizeIncrement)
|
||||||
{
|
{
|
||||||
lock (_writeLock)
|
lock (_writeLock)
|
||||||
{
|
{
|
||||||
if (_completed)
|
if (_completed)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
_outgoingFrame.PrepareWindowUpdate(streamId, sizeIncrement);
|
_outgoingFrame.PrepareWindowUpdate(streamId, sizeIncrement);
|
||||||
|
|
@ -413,13 +417,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
| Error Code (32) |
|
| Error Code (32) |
|
||||||
+---------------------------------------------------------------+
|
+---------------------------------------------------------------+
|
||||||
*/
|
*/
|
||||||
public Task WriteRstStreamAsync(int streamId, Http2ErrorCode errorCode)
|
public ValueTask<FlushResult> WriteRstStreamAsync(int streamId, Http2ErrorCode errorCode)
|
||||||
{
|
{
|
||||||
lock (_writeLock)
|
lock (_writeLock)
|
||||||
{
|
{
|
||||||
if (_completed)
|
if (_completed)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
_outgoingFrame.PrepareRstStream(streamId, errorCode);
|
_outgoingFrame.PrepareRstStream(streamId, errorCode);
|
||||||
|
|
@ -440,13 +444,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
| Value (32) |
|
| Value (32) |
|
||||||
+---------------------------------------------------------------+
|
+---------------------------------------------------------------+
|
||||||
*/
|
*/
|
||||||
public Task WriteSettingsAsync(IList<Http2PeerSetting> settings)
|
public ValueTask<FlushResult> WriteSettingsAsync(IList<Http2PeerSetting> settings)
|
||||||
{
|
{
|
||||||
lock (_writeLock)
|
lock (_writeLock)
|
||||||
{
|
{
|
||||||
if (_completed)
|
if (_completed)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
_outgoingFrame.PrepareSettings(Http2SettingsFrameFlags.NONE);
|
_outgoingFrame.PrepareSettings(Http2SettingsFrameFlags.NONE);
|
||||||
|
|
@ -473,13 +477,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
|
|
||||||
// No payload
|
// No payload
|
||||||
public Task WriteSettingsAckAsync()
|
public ValueTask<FlushResult> WriteSettingsAckAsync()
|
||||||
{
|
{
|
||||||
lock (_writeLock)
|
lock (_writeLock)
|
||||||
{
|
{
|
||||||
if (_completed)
|
if (_completed)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
_outgoingFrame.PrepareSettings(Http2SettingsFrameFlags.ACK);
|
_outgoingFrame.PrepareSettings(Http2SettingsFrameFlags.ACK);
|
||||||
|
|
@ -495,13 +499,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
| |
|
| |
|
||||||
+---------------------------------------------------------------+
|
+---------------------------------------------------------------+
|
||||||
*/
|
*/
|
||||||
public Task WritePingAsync(Http2PingFrameFlags flags, ReadOnlySequence<byte> payload)
|
public ValueTask<FlushResult> WritePingAsync(Http2PingFrameFlags flags, ReadOnlySequence<byte> payload)
|
||||||
{
|
{
|
||||||
lock (_writeLock)
|
lock (_writeLock)
|
||||||
{
|
{
|
||||||
if (_completed)
|
if (_completed)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
_outgoingFrame.PreparePing(Http2PingFrameFlags.ACK);
|
_outgoingFrame.PreparePing(Http2PingFrameFlags.ACK);
|
||||||
|
|
@ -525,13 +529,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
| Additional Debug Data (*) | (not implemented)
|
| Additional Debug Data (*) | (not implemented)
|
||||||
+---------------------------------------------------------------+
|
+---------------------------------------------------------------+
|
||||||
*/
|
*/
|
||||||
public Task WriteGoAwayAsync(int lastStreamId, Http2ErrorCode errorCode)
|
public ValueTask<FlushResult> WriteGoAwayAsync(int lastStreamId, Http2ErrorCode errorCode)
|
||||||
{
|
{
|
||||||
lock (_writeLock)
|
lock (_writeLock)
|
||||||
{
|
{
|
||||||
if (_completed)
|
if (_completed)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
_outgoingFrame.PrepareGoAway(lastStreamId, errorCode);
|
_outgoingFrame.PrepareGoAway(lastStreamId, errorCode);
|
||||||
|
|
@ -583,7 +587,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
output.Advance(Http2FrameReader.HeaderLength);
|
output.Advance(Http2FrameReader.HeaderLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task TimeFlushUnsynchronizedAsync()
|
private ValueTask<FlushResult> TimeFlushUnsynchronizedAsync()
|
||||||
{
|
{
|
||||||
var bytesWritten = _unflushedBytes;
|
var bytesWritten = _unflushedBytes;
|
||||||
_unflushedBytes = 0;
|
_unflushedBytes = 0;
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
private readonly Http2Stream _stream;
|
private readonly Http2Stream _stream;
|
||||||
private readonly object _dataWriterLock = new object();
|
private readonly object _dataWriterLock = new object();
|
||||||
private readonly Pipe _dataPipe;
|
private readonly Pipe _dataPipe;
|
||||||
private readonly Task _dataWriteProcessingTask;
|
private readonly ValueTask<FlushResult> _dataWriteProcessingTask;
|
||||||
private bool _startedWritingDataFrames;
|
private bool _startedWritingDataFrames;
|
||||||
private bool _completed;
|
private bool _completed;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
@ -88,18 +88,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task FlushAsync(CancellationToken cancellationToken)
|
public ValueTask<FlushResult> FlushAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
return Task.FromCanceled(cancellationToken);
|
return new ValueTask<FlushResult>(Task.FromCanceled<FlushResult>(cancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_dataWriterLock)
|
lock (_dataWriterLock)
|
||||||
{
|
{
|
||||||
if (_completed)
|
if (_completed)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_startedWritingDataFrames)
|
if (_startedWritingDataFrames)
|
||||||
|
|
@ -117,20 +117,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Write100ContinueAsync()
|
public ValueTask<FlushResult> Write100ContinueAsync()
|
||||||
{
|
{
|
||||||
lock (_dataWriterLock)
|
lock (_dataWriterLock)
|
||||||
{
|
{
|
||||||
if (_completed)
|
if (_completed)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _frameWriter.Write100ContinueAsync(_streamId);
|
return _frameWriter.Write100ContinueAsync(_streamId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void WriteResponseHeaders(int statusCode, string ReasonPhrase, HttpResponseHeaders responseHeaders)
|
public void WriteResponseHeaders(int statusCode, string ReasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk)
|
||||||
{
|
{
|
||||||
lock (_dataWriterLock)
|
lock (_dataWriterLock)
|
||||||
{
|
{
|
||||||
|
|
@ -164,17 +164,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
_startedWritingDataFrames = true;
|
_startedWritingDataFrames = true;
|
||||||
|
|
||||||
_dataPipe.Writer.Write(data);
|
_dataPipe.Writer.Write(data);
|
||||||
return _flusher.FlushAsync(this, cancellationToken);
|
return _flusher.FlushAsync(this, cancellationToken).AsTask();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteStreamSuffixAsync()
|
public ValueTask<FlushResult> WriteStreamSuffixAsync()
|
||||||
{
|
{
|
||||||
lock (_dataWriterLock)
|
lock (_dataWriterLock)
|
||||||
{
|
{
|
||||||
if (_completed)
|
if (_completed)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
_completed = true;
|
_completed = true;
|
||||||
|
|
@ -184,7 +184,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteRstStreamAsync(Http2ErrorCode error)
|
public ValueTask<FlushResult> WriteRstStreamAsync(Http2ErrorCode error)
|
||||||
{
|
{
|
||||||
lock (_dataWriterLock)
|
lock (_dataWriterLock)
|
||||||
{
|
{
|
||||||
|
|
@ -196,8 +196,76 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessDataWrites()
|
public void Advance(int bytes)
|
||||||
{
|
{
|
||||||
|
lock (_dataWriterLock)
|
||||||
|
{
|
||||||
|
_startedWritingDataFrames = true;
|
||||||
|
|
||||||
|
_dataPipe.Writer.Advance(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Span<byte> GetSpan(int sizeHint = 0)
|
||||||
|
{
|
||||||
|
lock (_dataWriterLock)
|
||||||
|
{
|
||||||
|
return _dataPipe.Writer.GetSpan(sizeHint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Memory<byte> GetMemory(int sizeHint = 0)
|
||||||
|
{
|
||||||
|
lock (_dataWriterLock)
|
||||||
|
{
|
||||||
|
return _dataPipe.Writer.GetMemory(sizeHint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CancelPendingFlush()
|
||||||
|
{
|
||||||
|
lock (_dataWriterLock)
|
||||||
|
{
|
||||||
|
_dataPipe.Writer.CancelPendingFlush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<FlushResult> WriteDataToPipeAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return new ValueTask<FlushResult>(Task.FromCanceled<FlushResult>(cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_dataWriterLock)
|
||||||
|
{
|
||||||
|
// This length check is important because we don't want to set _startedWritingDataFrames unless a data
|
||||||
|
// frame will actually be written causing the headers to be flushed.
|
||||||
|
if (_completed || data.Length == 0)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
_startedWritingDataFrames = true;
|
||||||
|
|
||||||
|
_dataPipe.Writer.Write(data);
|
||||||
|
return _flusher.FlushAsync(this, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueTask<FlushResult> IHttpOutputProducer.WriteChunkAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Complete()
|
||||||
|
{
|
||||||
|
// This will noop for now. See: https://github.com/aspnet/AspNetCore/issues/7370
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<FlushResult> ProcessDataWrites()
|
||||||
|
{
|
||||||
|
FlushResult flushResult = default;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ReadResult readResult;
|
ReadResult readResult;
|
||||||
|
|
@ -210,14 +278,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
{
|
{
|
||||||
if (readResult.Buffer.Length > 0)
|
if (readResult.Buffer.Length > 0)
|
||||||
{
|
{
|
||||||
await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream: false);
|
flushResult = await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _frameWriter.WriteResponseTrailers(_streamId, _stream.Trailers);
|
flushResult = await _frameWriter.WriteResponseTrailers(_streamId, _stream.Trailers);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream: readResult.IsCompleted);
|
flushResult = await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream: readResult.IsCompleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
_dataPipe.Reader.AdvanceTo(readResult.Buffer.End);
|
_dataPipe.Reader.AdvanceTo(readResult.Buffer.End);
|
||||||
|
|
@ -233,6 +301,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
|
|
||||||
_dataPipe.Reader.Complete();
|
_dataPipe.Reader.Complete();
|
||||||
|
|
||||||
|
return flushResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Pipe CreateDataPipe(MemoryPool<byte> pool)
|
private static Pipe CreateDataPipe(MemoryPool<byte> pool)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -18,11 +18,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
||||||
private readonly HttpRequestStream _emptyRequest;
|
private readonly HttpRequestStream _emptyRequest;
|
||||||
private readonly Stream _upgradeStream;
|
private readonly Stream _upgradeStream;
|
||||||
|
|
||||||
public Streams(IHttpBodyControlFeature bodyControl, IHttpResponseControl httpResponseControl)
|
public Streams(IHttpBodyControlFeature bodyControl, HttpResponsePipeWriter writer)
|
||||||
{
|
{
|
||||||
_request = new HttpRequestStream(bodyControl);
|
_request = new HttpRequestStream(bodyControl);
|
||||||
_emptyRequest = new HttpRequestStream(bodyControl);
|
_emptyRequest = new HttpRequestStream(bodyControl);
|
||||||
_response = new HttpResponseStream(bodyControl, httpResponseControl);
|
_response = new HttpResponseStream(bodyControl, writer);
|
||||||
_upgradeableResponse = new WrappingStream(_response);
|
_upgradeableResponse = new WrappingStream(_response);
|
||||||
_upgradeStream = new HttpUpgradeStream(_request, _response);
|
_upgradeStream = new HttpUpgradeStream(_request, _response);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
||||||
private readonly IKestrelTrace _log;
|
private readonly IKestrelTrace _log;
|
||||||
|
|
||||||
private readonly object _flushLock = new object();
|
private readonly object _flushLock = new object();
|
||||||
private Task _lastFlushTask = Task.CompletedTask;
|
private Task<FlushResult> _lastFlushTask = null;
|
||||||
|
|
||||||
public TimingPipeFlusher(
|
public TimingPipeFlusher(
|
||||||
PipeWriter writer,
|
PipeWriter writer,
|
||||||
|
|
@ -34,22 +34,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
||||||
_log = log;
|
_log = log;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task FlushAsync()
|
public ValueTask<FlushResult> FlushAsync()
|
||||||
{
|
{
|
||||||
return FlushAsync(outputAborter: null, cancellationToken: default);
|
return FlushAsync(outputAborter: null, cancellationToken: default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task FlushAsync(IHttpOutputAborter outputAborter, CancellationToken cancellationToken)
|
public ValueTask<FlushResult> FlushAsync(IHttpOutputAborter outputAborter, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return FlushAsync(minRate: null, count: 0, outputAborter: outputAborter, cancellationToken: cancellationToken);
|
return FlushAsync(minRate: null, count: 0, outputAborter: outputAborter, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task FlushAsync(MinDataRate minRate, long count)
|
public ValueTask<FlushResult> FlushAsync(MinDataRate minRate, long count)
|
||||||
{
|
{
|
||||||
return FlushAsync(minRate, count, outputAborter: null, cancellationToken: default);
|
return FlushAsync(minRate, count, outputAborter: null, cancellationToken: default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task FlushAsync(MinDataRate minRate, long count, IHttpOutputAborter outputAborter, CancellationToken cancellationToken)
|
public ValueTask<FlushResult> FlushAsync(MinDataRate minRate, long count, IHttpOutputAborter outputAborter, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var flushValueTask = _writer.FlushAsync(cancellationToken);
|
var flushValueTask = _writer.FlushAsync(cancellationToken);
|
||||||
|
|
||||||
|
|
@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
||||||
|
|
||||||
if (flushValueTask.IsCompletedSuccessfully)
|
if (flushValueTask.IsCompletedSuccessfully)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return new ValueTask<FlushResult>(flushValueTask.Result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/dotnet/corefxlab/issues/1334
|
// https://github.com/dotnet/corefxlab/issues/1334
|
||||||
|
|
@ -70,7 +70,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
||||||
// we find previous flush Task which still accounts for any newly committed bytes and await that.
|
// we find previous flush Task which still accounts for any newly committed bytes and await that.
|
||||||
lock (_flushLock)
|
lock (_flushLock)
|
||||||
{
|
{
|
||||||
if (_lastFlushTask.IsCompleted)
|
if (_lastFlushTask == null || _lastFlushTask.IsCompleted)
|
||||||
{
|
{
|
||||||
_lastFlushTask = flushValueTask.AsTask();
|
_lastFlushTask = flushValueTask.AsTask();
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +79,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task TimeFlushAsync(MinDataRate minRate, long count, IHttpOutputAborter outputAborter, CancellationToken cancellationToken)
|
private async ValueTask<FlushResult> TimeFlushAsync(MinDataRate minRate, long count, IHttpOutputAborter outputAborter, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (minRate != null)
|
if (minRate != null)
|
||||||
{
|
{
|
||||||
|
|
@ -88,7 +88,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _lastFlushTask;
|
return await _lastFlushTask;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException ex) when (outputAborter != null)
|
catch (OperationCanceledException ex) when (outputAborter != null)
|
||||||
{
|
{
|
||||||
|
|
@ -99,13 +99,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
||||||
// A canceled token is the only reason flush should ever throw.
|
// A canceled token is the only reason flush should ever throw.
|
||||||
_log.LogError(0, ex, $"Unexpected exception in {nameof(TimingPipeFlusher)}.{nameof(TimeFlushAsync)}.");
|
_log.LogError(0, ex, $"Unexpected exception in {nameof(TimingPipeFlusher)}.{nameof(TimeFlushAsync)}.");
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
if (minRate != null)
|
|
||||||
{
|
{
|
||||||
_timeoutControl.StopTimingWrite();
|
if (minRate != null)
|
||||||
|
{
|
||||||
|
_timeoutControl.StopTimingWrite();
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
}
|
}
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
return default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2239,6 +2239,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static string FormatServerShutdownDuringConnectionInitialization()
|
internal static string FormatServerShutdownDuringConnectionInitialization()
|
||||||
=> GetString("ServerShutdownDuringConnectionInitialization");
|
=> GetString("ServerShutdownDuringConnectionInitialization");
|
||||||
|
/// <summary>
|
||||||
|
/// Cannot call GetMemory() until response has started. Call HttpResponse.StartAsync() before calling GetMemory().
|
||||||
|
/// </summary>
|
||||||
|
internal static string StartAsyncBeforeGetMemory
|
||||||
|
{
|
||||||
|
get => GetString("StartAsyncBeforeGetMemory");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cannot call GetMemory() until response has started. Call HttpResponse.StartAsync() before calling GetMemory().
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatStartAsyncBeforeGetMemory()
|
||||||
|
=> GetString("StartAsyncBeforeGetMemory");
|
||||||
|
|
||||||
private static string GetString(string name, params string[] formatterNames)
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
{
|
{
|
||||||
_collection[typeof(IHttpRequestFeature)] = CreateHttp1Connection();
|
_collection[typeof(IHttpRequestFeature)] = CreateHttp1Connection();
|
||||||
_collection[typeof(IHttpResponseFeature)] = CreateHttp1Connection();
|
_collection[typeof(IHttpResponseFeature)] = CreateHttp1Connection();
|
||||||
|
_collection[typeof(IResponseBodyPipeFeature)] = CreateHttp1Connection();
|
||||||
_collection[typeof(IHttpRequestIdentifierFeature)] = CreateHttp1Connection();
|
_collection[typeof(IHttpRequestIdentifierFeature)] = CreateHttp1Connection();
|
||||||
_collection[typeof(IHttpRequestLifetimeFeature)] = CreateHttp1Connection();
|
_collection[typeof(IHttpRequestLifetimeFeature)] = CreateHttp1Connection();
|
||||||
_collection[typeof(IHttpConnectionFeature)] = CreateHttp1Connection();
|
_collection[typeof(IHttpConnectionFeature)] = CreateHttp1Connection();
|
||||||
|
|
@ -136,6 +137,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
{
|
{
|
||||||
_collection.Set<IHttpRequestFeature>(CreateHttp1Connection());
|
_collection.Set<IHttpRequestFeature>(CreateHttp1Connection());
|
||||||
_collection.Set<IHttpResponseFeature>(CreateHttp1Connection());
|
_collection.Set<IHttpResponseFeature>(CreateHttp1Connection());
|
||||||
|
_collection.Set<IResponseBodyPipeFeature>(CreateHttp1Connection());
|
||||||
_collection.Set<IHttpRequestIdentifierFeature>(CreateHttp1Connection());
|
_collection.Set<IHttpRequestIdentifierFeature>(CreateHttp1Connection());
|
||||||
_collection.Set<IHttpRequestLifetimeFeature>(CreateHttp1Connection());
|
_collection.Set<IHttpRequestLifetimeFeature>(CreateHttp1Connection());
|
||||||
_collection.Set<IHttpConnectionFeature>(CreateHttp1Connection());
|
_collection.Set<IHttpConnectionFeature>(CreateHttp1Connection());
|
||||||
|
|
@ -173,6 +175,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
{
|
{
|
||||||
Assert.Same(_collection.Get<IHttpRequestFeature>(), _collection[typeof(IHttpRequestFeature)]);
|
Assert.Same(_collection.Get<IHttpRequestFeature>(), _collection[typeof(IHttpRequestFeature)]);
|
||||||
Assert.Same(_collection.Get<IHttpResponseFeature>(), _collection[typeof(IHttpResponseFeature)]);
|
Assert.Same(_collection.Get<IHttpResponseFeature>(), _collection[typeof(IHttpResponseFeature)]);
|
||||||
|
Assert.Same(_collection.Get<IResponseBodyPipeFeature>(), _collection[typeof(IResponseBodyPipeFeature)]);
|
||||||
Assert.Same(_collection.Get<IHttpRequestIdentifierFeature>(), _collection[typeof(IHttpRequestIdentifierFeature)]);
|
Assert.Same(_collection.Get<IHttpRequestIdentifierFeature>(), _collection[typeof(IHttpRequestIdentifierFeature)]);
|
||||||
Assert.Same(_collection.Get<IHttpRequestLifetimeFeature>(), _collection[typeof(IHttpRequestLifetimeFeature)]);
|
Assert.Same(_collection.Get<IHttpRequestLifetimeFeature>(), _collection[typeof(IHttpRequestLifetimeFeature)]);
|
||||||
Assert.Same(_collection.Get<IHttpConnectionFeature>(), _collection[typeof(IHttpConnectionFeature)]);
|
Assert.Same(_collection.Get<IHttpConnectionFeature>(), _collection[typeof(IHttpConnectionFeature)]);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
|
{
|
||||||
|
public class HttpResponsePipeWriterTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void OnReaderCompletedThrowsNotSupported()
|
||||||
|
{
|
||||||
|
var pipeWriter = CreateHttpResponsePipeWriter();
|
||||||
|
Assert.Throws<NotSupportedException>(() => pipeWriter.OnReaderCompleted((a, b) => { }, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AdvanceAfterStopAcceptingWritesThrowsObjectDisposedException()
|
||||||
|
{
|
||||||
|
var pipeWriter = CreateHttpResponsePipeWriter();
|
||||||
|
pipeWriter.StartAcceptingWrites();
|
||||||
|
pipeWriter.StopAcceptingWrites();
|
||||||
|
var ex = Assert.Throws<ObjectDisposedException>(() => { pipeWriter.Advance(1); });
|
||||||
|
Assert.Contains(CoreStrings.WritingToResponseBodyAfterResponseCompleted, ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMemoryAfterStopAcceptingWritesThrowsObjectDisposedException()
|
||||||
|
{
|
||||||
|
var pipeWriter = CreateHttpResponsePipeWriter();
|
||||||
|
pipeWriter.StartAcceptingWrites();
|
||||||
|
pipeWriter.StopAcceptingWrites();
|
||||||
|
var ex = Assert.Throws<ObjectDisposedException>(() => { pipeWriter.GetMemory(); });
|
||||||
|
Assert.Contains(CoreStrings.WritingToResponseBodyAfterResponseCompleted, ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetSpanAfterStopAcceptingWritesThrowsObjectDisposedException()
|
||||||
|
{
|
||||||
|
var pipeWriter = CreateHttpResponsePipeWriter();
|
||||||
|
pipeWriter.StartAcceptingWrites();
|
||||||
|
pipeWriter.StopAcceptingWrites();
|
||||||
|
var ex = Assert.Throws<ObjectDisposedException>(() => { pipeWriter.GetSpan(); });
|
||||||
|
Assert.Contains(CoreStrings.WritingToResponseBodyAfterResponseCompleted, ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FlushAsyncAfterStopAcceptingWritesThrowsObjectDisposedException()
|
||||||
|
{
|
||||||
|
var pipeWriter = CreateHttpResponsePipeWriter();
|
||||||
|
pipeWriter.StartAcceptingWrites();
|
||||||
|
pipeWriter.StopAcceptingWrites();
|
||||||
|
var ex = Assert.Throws<ObjectDisposedException>(() => { pipeWriter.FlushAsync(); });
|
||||||
|
Assert.Contains(CoreStrings.WritingToResponseBodyAfterResponseCompleted, ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WriteAsyncAfterStopAcceptingWritesThrowsObjectDisposedException()
|
||||||
|
{
|
||||||
|
var pipeWriter = CreateHttpResponsePipeWriter();
|
||||||
|
pipeWriter.StartAcceptingWrites();
|
||||||
|
pipeWriter.StopAcceptingWrites();
|
||||||
|
var ex = Assert.Throws<ObjectDisposedException>(() => { pipeWriter.WriteAsync(new Memory<byte>()); });
|
||||||
|
Assert.Contains(CoreStrings.WritingToResponseBodyAfterResponseCompleted, ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CompleteCallsStopAcceptingWrites()
|
||||||
|
{
|
||||||
|
var pipeWriter = CreateHttpResponsePipeWriter();
|
||||||
|
pipeWriter.Complete();
|
||||||
|
var ex = Assert.Throws<ObjectDisposedException>(() => { pipeWriter.WriteAsync(new Memory<byte>()); });
|
||||||
|
Assert.Contains(CoreStrings.WritingToResponseBodyAfterResponseCompleted, ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpResponsePipeWriter CreateHttpResponsePipeWriter()
|
||||||
|
{
|
||||||
|
return new HttpResponsePipeWriter(Mock.Of<IHttpResponseControl>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.IO.Pipelines;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Tests.TestHelpers;
|
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -18,77 +18,77 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanReadReturnsFalse()
|
public void CanReadReturnsFalse()
|
||||||
{
|
{
|
||||||
var stream = new HttpResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockHttpResponseControl());
|
var stream = CreateHttpResponseStream();
|
||||||
Assert.False(stream.CanRead);
|
Assert.False(stream.CanRead);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanSeekReturnsFalse()
|
public void CanSeekReturnsFalse()
|
||||||
{
|
{
|
||||||
var stream = new HttpResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockHttpResponseControl());
|
var stream = CreateHttpResponseStream();
|
||||||
Assert.False(stream.CanSeek);
|
Assert.False(stream.CanSeek);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanWriteReturnsTrue()
|
public void CanWriteReturnsTrue()
|
||||||
{
|
{
|
||||||
var stream = new HttpResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockHttpResponseControl());
|
var stream = CreateHttpResponseStream();
|
||||||
Assert.True(stream.CanWrite);
|
Assert.True(stream.CanWrite);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ReadThrows()
|
public void ReadThrows()
|
||||||
{
|
{
|
||||||
var stream = new HttpResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockHttpResponseControl());
|
var stream = CreateHttpResponseStream();
|
||||||
Assert.Throws<NotSupportedException>(() => stream.Read(new byte[1], 0, 1));
|
Assert.Throws<NotSupportedException>(() => stream.Read(new byte[1], 0, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ReadByteThrows()
|
public void ReadByteThrows()
|
||||||
{
|
{
|
||||||
var stream = new HttpResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockHttpResponseControl());
|
var stream = CreateHttpResponseStream();
|
||||||
Assert.Throws<NotSupportedException>(() => stream.ReadByte());
|
Assert.Throws<NotSupportedException>(() => stream.ReadByte());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ReadAsyncThrows()
|
public async Task ReadAsyncThrows()
|
||||||
{
|
{
|
||||||
var stream = new HttpResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockHttpResponseControl());
|
var stream = CreateHttpResponseStream();
|
||||||
await Assert.ThrowsAsync<NotSupportedException>(() => stream.ReadAsync(new byte[1], 0, 1));
|
await Assert.ThrowsAsync<NotSupportedException>(() => stream.ReadAsync(new byte[1], 0, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void BeginReadThrows()
|
public void BeginReadThrows()
|
||||||
{
|
{
|
||||||
var stream = new HttpResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockHttpResponseControl());
|
var stream = CreateHttpResponseStream();
|
||||||
Assert.Throws<NotSupportedException>(() => stream.BeginRead(new byte[1], 0, 1, null, null));
|
Assert.Throws<NotSupportedException>(() => stream.BeginRead(new byte[1], 0, 1, null, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SeekThrows()
|
public void SeekThrows()
|
||||||
{
|
{
|
||||||
var stream = new HttpResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockHttpResponseControl());
|
var stream = CreateHttpResponseStream();
|
||||||
Assert.Throws<NotSupportedException>(() => stream.Seek(0, SeekOrigin.Begin));
|
Assert.Throws<NotSupportedException>(() => stream.Seek(0, SeekOrigin.Begin));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void LengthThrows()
|
public void LengthThrows()
|
||||||
{
|
{
|
||||||
var stream = new HttpResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockHttpResponseControl());
|
var stream = CreateHttpResponseStream();
|
||||||
Assert.Throws<NotSupportedException>(() => stream.Length);
|
Assert.Throws<NotSupportedException>(() => stream.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SetLengthThrows()
|
public void SetLengthThrows()
|
||||||
{
|
{
|
||||||
var stream = new HttpResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockHttpResponseControl());
|
var stream = CreateHttpResponseStream();
|
||||||
Assert.Throws<NotSupportedException>(() => stream.SetLength(0));
|
Assert.Throws<NotSupportedException>(() => stream.SetLength(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PositionThrows()
|
public void PositionThrows()
|
||||||
{
|
{
|
||||||
var stream = new HttpResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockHttpResponseControl());
|
var stream = CreateHttpResponseStream();
|
||||||
Assert.Throws<NotSupportedException>(() => stream.Position);
|
Assert.Throws<NotSupportedException>(() => stream.Position);
|
||||||
Assert.Throws<NotSupportedException>(() => stream.Position = 0);
|
Assert.Throws<NotSupportedException>(() => stream.Position = 0);
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +96,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void StopAcceptingWritesCausesWriteToThrowObjectDisposedException()
|
public void StopAcceptingWritesCausesWriteToThrowObjectDisposedException()
|
||||||
{
|
{
|
||||||
var stream = new HttpResponseStream(Mock.Of<IHttpBodyControlFeature>(), Mock.Of<IHttpResponseControl>());
|
var pipeWriter = new HttpResponsePipeWriter(Mock.Of<IHttpResponseControl>());
|
||||||
|
var stream = new HttpResponseStream(Mock.Of<IHttpBodyControlFeature>(), pipeWriter);
|
||||||
stream.StartAcceptingWrites();
|
stream.StartAcceptingWrites();
|
||||||
stream.StopAcceptingWrites();
|
stream.StopAcceptingWrites();
|
||||||
var ex = Assert.Throws<ObjectDisposedException>(() => { stream.WriteAsync(new byte[1], 0, 1); });
|
var ex = Assert.Throws<ObjectDisposedException>(() => { stream.WriteAsync(new byte[1], 0, 1); });
|
||||||
|
|
@ -110,9 +111,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
var mockBodyControl = new Mock<IHttpBodyControlFeature>();
|
var mockBodyControl = new Mock<IHttpBodyControlFeature>();
|
||||||
mockBodyControl.Setup(m => m.AllowSynchronousIO).Returns(() => allowSynchronousIO);
|
mockBodyControl.Setup(m => m.AllowSynchronousIO).Returns(() => allowSynchronousIO);
|
||||||
var mockHttpResponseControl = new Mock<IHttpResponseControl>();
|
var mockHttpResponseControl = new Mock<IHttpResponseControl>();
|
||||||
mockHttpResponseControl.Setup(m => m.WriteAsync(It.IsAny<ReadOnlyMemory<byte>>(), CancellationToken.None)).Returns(Task.CompletedTask);
|
mockHttpResponseControl.Setup(m => m.WritePipeAsync(It.IsAny<ReadOnlyMemory<byte>>(), CancellationToken.None)).Returns(new ValueTask<FlushResult>(new FlushResult()));
|
||||||
|
|
||||||
var stream = new HttpResponseStream(mockBodyControl.Object, mockHttpResponseControl.Object);
|
var pipeWriter = new HttpResponsePipeWriter(mockHttpResponseControl.Object);
|
||||||
|
var stream = new HttpResponseStream(mockBodyControl.Object, pipeWriter);
|
||||||
stream.StartAcceptingWrites();
|
stream.StartAcceptingWrites();
|
||||||
|
|
||||||
// WriteAsync doesn't throw.
|
// WriteAsync doesn't throw.
|
||||||
|
|
@ -125,5 +127,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
// If IHttpBodyControlFeature.AllowSynchronousIO is true, Write no longer throws.
|
// If IHttpBodyControlFeature.AllowSynchronousIO is true, Write no longer throws.
|
||||||
stream.Write(new byte[1], 0, 1);
|
stream.Write(new byte[1], 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static HttpResponseStream CreateHttpResponseStream()
|
||||||
|
{
|
||||||
|
var pipeWriter = new HttpResponsePipeWriter(Mock.Of<IHttpResponseControl>());
|
||||||
|
return new HttpResponseStream(Mock.Of<IHttpBodyControlFeature>(), pipeWriter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@
|
||||||
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.pfx" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
|
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.pfx" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="C:\Users\jukotali\code\aspnetcore\src\Servers\Kestrel\shared\test\HttpResponseWritingExtensions.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Microsoft.AspNetCore.Http" />
|
<Reference Include="Microsoft.AspNetCore.Http" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Core" />
|
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Core" />
|
||||||
|
|
|
||||||
|
|
@ -89,14 +89,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
connectionContext,
|
connectionContext,
|
||||||
serviceContext.Log,
|
serviceContext.Log,
|
||||||
Mock.Of<ITimeoutControl>(),
|
Mock.Of<ITimeoutControl>(),
|
||||||
Mock.Of<IHttpMinResponseDataRateFeature>());
|
Mock.Of<IHttpMinResponseDataRateFeature>(),
|
||||||
|
_memoryPool);
|
||||||
|
|
||||||
return socketOutput;
|
return socketOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestHttpOutputProducer : Http1OutputProducer
|
private class TestHttpOutputProducer : Http1OutputProducer
|
||||||
{
|
{
|
||||||
public TestHttpOutputProducer(Pipe pipe, string connectionId, ConnectionContext connectionContext, IKestrelTrace log, ITimeoutControl timeoutControl, IHttpMinResponseDataRateFeature minResponseDataRateFeature) : base(pipe.Writer, connectionId, connectionContext, log, timeoutControl, minResponseDataRateFeature)
|
public TestHttpOutputProducer(Pipe pipe, string connectionId, ConnectionContext connectionContext, IKestrelTrace log, ITimeoutControl timeoutControl, IHttpMinResponseDataRateFeature minResponseDataRateFeature, MemoryPool<byte> memoryPool) : base(pipe.Writer, connectionId, connectionContext, log, timeoutControl, minResponseDataRateFeature, memoryPool)
|
||||||
{
|
{
|
||||||
Pipe = pipe;
|
Pipe = pipe;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task StreamsThrowAfterAbort()
|
public async Task StreamsThrowAfterAbort()
|
||||||
{
|
{
|
||||||
var streams = new Streams(Mock.Of<IHttpBodyControlFeature>(), Mock.Of<IHttpResponseControl>());
|
var streams = new Streams(Mock.Of<IHttpBodyControlFeature>(), new HttpResponsePipeWriter(Mock.Of<IHttpResponseControl>()));
|
||||||
var (request, response) = streams.Start(new MockMessageBody());
|
var (request, response) = streams.Start(new MockMessageBody());
|
||||||
|
|
||||||
var ex = new Exception("My error");
|
var ex = new Exception("My error");
|
||||||
|
|
@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task StreamsThrowOnAbortAfterUpgrade()
|
public async Task StreamsThrowOnAbortAfterUpgrade()
|
||||||
{
|
{
|
||||||
var streams = new Streams(Mock.Of<IHttpBodyControlFeature>(), Mock.Of<IHttpResponseControl>());
|
var streams = new Streams(Mock.Of<IHttpBodyControlFeature>(), new HttpResponsePipeWriter(Mock.Of<IHttpResponseControl>()));
|
||||||
var (request, response) = streams.Start(new MockMessageBody(upgradeable: true));
|
var (request, response) = streams.Start(new MockMessageBody(upgradeable: true));
|
||||||
|
|
||||||
var upgrade = streams.Upgrade();
|
var upgrade = streams.Upgrade();
|
||||||
|
|
@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task StreamsThrowOnUpgradeAfterAbort()
|
public async Task StreamsThrowOnUpgradeAfterAbort()
|
||||||
{
|
{
|
||||||
var streams = new Streams(Mock.Of<IHttpBodyControlFeature>(), Mock.Of<IHttpResponseControl>());
|
var streams = new Streams(Mock.Of<IHttpBodyControlFeature>(), new HttpResponsePipeWriter(Mock.Of<IHttpResponseControl>()));
|
||||||
|
|
||||||
var (request, response) = streams.Start(new MockMessageBody(upgradeable: true));
|
var (request, response) = streams.Start(new MockMessageBody(upgradeable: true));
|
||||||
var ex = new Exception("My error");
|
var ex = new Exception("My error");
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests.TestHelpers
|
|
||||||
{
|
|
||||||
public class MockHttpResponseControl : IHttpResponseControl
|
|
||||||
{
|
|
||||||
public Task FlushAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ProduceContinue()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task WriteAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
@ -38,5 +39,39 @@ namespace Microsoft.AspNetCore.Testing
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task EchoAppPipeWriter(HttpContext httpContext)
|
||||||
|
{
|
||||||
|
var request = httpContext.Request;
|
||||||
|
var response = httpContext.Response;
|
||||||
|
var buffer = new byte[httpContext.Request.ContentLength ?? 0];
|
||||||
|
|
||||||
|
if (buffer.Length > 0)
|
||||||
|
{
|
||||||
|
await request.Body.ReadUntilEndAsync(buffer).DefaultTimeout();
|
||||||
|
await response.StartAsync();
|
||||||
|
var memory = response.BodyPipe.GetMemory(buffer.Length);
|
||||||
|
buffer.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(buffer.Length);
|
||||||
|
await response.BodyPipe.FlushAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task EchoAppPipeWriterChunked(HttpContext httpContext)
|
||||||
|
{
|
||||||
|
var request = httpContext.Request;
|
||||||
|
var response = httpContext.Response;
|
||||||
|
var data = new MemoryStream();
|
||||||
|
await request.Body.CopyToAsync(data);
|
||||||
|
var bytes = data.ToArray();
|
||||||
|
|
||||||
|
response.Headers["Content-Length"] = bytes.Length.ToString();
|
||||||
|
await response.StartAsync();
|
||||||
|
|
||||||
|
var memory = response.BodyPipe.GetMemory(bytes.Length);
|
||||||
|
bytes.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(bytes.Length);
|
||||||
|
await response.BodyPipe.FlushAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Https;
|
using Microsoft.AspNetCore.Server.Kestrel.Https;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
|
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Tests;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
using Microsoft.AspNetCore.Testing.xunit;
|
using Microsoft.AspNetCore.Testing.xunit;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
@ -62,7 +63,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
|
|
||||||
for (int i = 0; i < 1024; i++)
|
for (int i = 0; i < 1024; i++)
|
||||||
{
|
{
|
||||||
await context.Response.Body.WriteAsync(bytes, 0, bytes.Length);
|
await context.Response.BodyPipe.WriteAsync(new Memory<byte>(bytes, 0, bytes.Length));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -208,7 +209,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await response.WriteAsync(largeString, lifetime.RequestAborted);
|
await response.WriteAsync(largeString, cancellationToken: lifetime.RequestAborted);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -297,7 +298,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
{
|
{
|
||||||
for (var i = 0; i < 1000; i++)
|
for (var i = 0; i < 1000; i++)
|
||||||
{
|
{
|
||||||
await context.Response.Body.WriteAsync(scratchBuffer, 0, scratchBuffer.Length, context.RequestAborted);
|
await context.Response.BodyPipe.WriteAsync(new Memory<byte>(scratchBuffer, 0, scratchBuffer.Length), context.RequestAborted);
|
||||||
await Task.Delay(10);
|
await Task.Delay(10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -499,7 +500,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
{
|
{
|
||||||
for (; i < chunks; i++)
|
for (; i < chunks; i++)
|
||||||
{
|
{
|
||||||
await context.Response.Body.WriteAsync(chunkData, 0, chunkData.Length, context.RequestAborted);
|
await context.Response.BodyPipe.WriteAsync(new Memory<byte>(chunkData, 0, chunkData.Length), context.RequestAborted);
|
||||||
await Task.Yield();
|
await Task.Yield();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -606,7 +607,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
{
|
{
|
||||||
for (var i = 0; i < chunks; i++)
|
for (var i = 0; i < chunks; i++)
|
||||||
{
|
{
|
||||||
await context.Response.Body.WriteAsync(chunkData, 0, chunkData.Length, context.RequestAborted);
|
await context.Response.BodyPipe.WriteAsync(new Memory<byte>(chunkData, 0, chunkData.Length), context.RequestAborted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
|
|
@ -770,7 +771,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
|
|
||||||
for (var i = 0; i < chunkCount; i++)
|
for (var i = 0; i < chunkCount; i++)
|
||||||
{
|
{
|
||||||
await context.Response.Body.WriteAsync(chunkData, 0, chunkData.Length, context.RequestAborted);
|
await context.Response.BodyPipe.WriteAsync(new Memory<byte>(chunkData, 0, chunkData.Length), context.RequestAborted);
|
||||||
}
|
}
|
||||||
|
|
||||||
appFuncCompleted.SetResult(null);
|
appFuncCompleted.SetResult(null);
|
||||||
|
|
@ -847,7 +848,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
context.Response.Headers[$"X-Custom-Header"] = headerStringValues;
|
context.Response.Headers[$"X-Custom-Header"] = headerStringValues;
|
||||||
context.Response.ContentLength = 0;
|
context.Response.ContentLength = 0;
|
||||||
|
|
||||||
await context.Response.Body.FlushAsync();
|
await context.Response.BodyPipe.FlushAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var server = new TestServer(App, testContext, listenOptions))
|
using (var server = new TestServer(App, testContext, listenOptions))
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
using (var server = new TestServer(async httpContext =>
|
using (var server = new TestServer(async httpContext =>
|
||||||
{
|
{
|
||||||
var response = httpContext.Response;
|
var response = httpContext.Response;
|
||||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello "), 0, 6);
|
await response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("Hello "), 0, 6));
|
||||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6);
|
await response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("World!"), 0, 6));
|
||||||
}, testContext))
|
}, testContext))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
|
|
@ -162,9 +162,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
using (var server = new TestServer(async httpContext =>
|
using (var server = new TestServer(async httpContext =>
|
||||||
{
|
{
|
||||||
var response = httpContext.Response;
|
var response = httpContext.Response;
|
||||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello "), 0, 6);
|
await response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("Hello "), 0, 6));
|
||||||
await response.Body.WriteAsync(new byte[0], 0, 0);
|
await response.BodyPipe.WriteAsync(new Memory<byte>(new byte[0], 0, 0));
|
||||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6);
|
await response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("World!"), 0, 6));
|
||||||
}, testContext))
|
}, testContext))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
|
|
@ -244,7 +244,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
using (var server = new TestServer(async httpContext =>
|
using (var server = new TestServer(async httpContext =>
|
||||||
{
|
{
|
||||||
var response = httpContext.Response;
|
var response = httpContext.Response;
|
||||||
await response.Body.WriteAsync(new byte[0], 0, 0);
|
await response.BodyPipe.WriteAsync(new Memory<byte>(new byte[0], 0, 0));
|
||||||
}, testContext))
|
}, testContext))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
|
|
@ -275,7 +275,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
using (var server = new TestServer(async httpContext =>
|
using (var server = new TestServer(async httpContext =>
|
||||||
{
|
{
|
||||||
var response = httpContext.Response;
|
var response = httpContext.Response;
|
||||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World!"), 0, 12);
|
await response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("Hello World!"), 0, 12));
|
||||||
throw new Exception();
|
throw new Exception();
|
||||||
}, testContext))
|
}, testContext))
|
||||||
{
|
{
|
||||||
|
|
@ -309,7 +309,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
using (var server = new TestServer(async httpContext =>
|
using (var server = new TestServer(async httpContext =>
|
||||||
{
|
{
|
||||||
var response = httpContext.Response;
|
var response = httpContext.Response;
|
||||||
await response.Body.WriteAsync(new byte[0], 0, 0);
|
await response.BodyPipe.WriteAsync(new Memory<byte>(new byte[0], 0, 0));
|
||||||
throw new Exception();
|
throw new Exception();
|
||||||
}, testContext))
|
}, testContext))
|
||||||
{
|
{
|
||||||
|
|
@ -344,12 +344,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
using (var server = new TestServer(async httpContext =>
|
using (var server = new TestServer(async httpContext =>
|
||||||
{
|
{
|
||||||
var response = httpContext.Response;
|
var response = httpContext.Response;
|
||||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello "), 0, 6);
|
await response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("Hello "), 0, 6));
|
||||||
|
|
||||||
// Don't complete response until client has received the first chunk.
|
// Don't complete response until client has received the first chunk.
|
||||||
await flushWh.Task.DefaultTimeout();
|
await flushWh.Task.DefaultTimeout();
|
||||||
|
|
||||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6);
|
await response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("World!"), 0, 6));
|
||||||
}, testContext))
|
}, testContext))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
|
|
@ -391,9 +391,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
var response = httpContext.Response;
|
var response = httpContext.Response;
|
||||||
response.Headers["Transfer-Encoding"] = "chunked";
|
response.Headers["Transfer-Encoding"] = "chunked";
|
||||||
|
|
||||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("6\r\nHello \r\n"), 0, 11);
|
await response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("6\r\nHello \r\n"), 0, 11));
|
||||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("6\r\nWorld!\r\n"), 0, 11);
|
await response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("6\r\nWorld!\r\n"), 0, 11));
|
||||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("0\r\n\r\n"), 0, 5);
|
await response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("0\r\n\r\n"), 0, 5));
|
||||||
}, testContext))
|
}, testContext))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
|
|
@ -420,6 +420,416 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
await server.StopAsync();
|
await server.StopAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ChunksWithGetMemoryBeforeFirstFlushStillFlushes()
|
||||||
|
{
|
||||||
|
var testContext = new TestServiceContext(LoggerFactory);
|
||||||
|
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
var response = httpContext.Response;
|
||||||
|
await response.StartAsync();
|
||||||
|
var memory = response.BodyPipe.GetMemory();
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("Hello ");
|
||||||
|
fisrtPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
|
||||||
|
memory = response.BodyPipe.GetMemory();
|
||||||
|
var secondPartOfResponse = Encoding.ASCII.GetBytes("World!");
|
||||||
|
secondPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
|
||||||
|
await response.BodyPipe.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 ChunksWithGetMemoryLargeWriteBeforeFirstFlush()
|
||||||
|
{
|
||||||
|
var testContext = new TestServiceContext(LoggerFactory);
|
||||||
|
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
var response = httpContext.Response;
|
||||||
|
await response.StartAsync();
|
||||||
|
|
||||||
|
var memory = response.BodyPipe.GetMemory(5000); // This will return 4089
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes(new string('a', memory.Length));
|
||||||
|
fisrtPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(memory.Length);
|
||||||
|
|
||||||
|
memory = response.BodyPipe.GetMemory();
|
||||||
|
var secondPartOfResponse = Encoding.ASCII.GetBytes("World!");
|
||||||
|
secondPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
|
||||||
|
await response.BodyPipe.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",
|
||||||
|
"",
|
||||||
|
"ff9",
|
||||||
|
new string('a', 4089),
|
||||||
|
"6",
|
||||||
|
"World!",
|
||||||
|
"0",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ChunksWithGetMemoryWithInitialFlushWorks()
|
||||||
|
{
|
||||||
|
var testContext = new TestServiceContext(LoggerFactory);
|
||||||
|
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
var response = httpContext.Response;
|
||||||
|
|
||||||
|
await response.BodyPipe.FlushAsync();
|
||||||
|
|
||||||
|
var memory = response.BodyPipe.GetMemory(5000); // This will return 4089
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes(new string('a', memory.Length));
|
||||||
|
fisrtPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(memory.Length);
|
||||||
|
|
||||||
|
memory = response.BodyPipe.GetMemory();
|
||||||
|
var secondPartOfResponse = Encoding.ASCII.GetBytes("World!");
|
||||||
|
secondPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
|
||||||
|
await response.BodyPipe.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",
|
||||||
|
"",
|
||||||
|
"ff9",
|
||||||
|
new string('a', 4089),
|
||||||
|
"6",
|
||||||
|
"World!",
|
||||||
|
"0",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ChunkGetMemoryMultipleAdvance()
|
||||||
|
{
|
||||||
|
var testContext = new TestServiceContext(LoggerFactory);
|
||||||
|
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
var response = httpContext.Response;
|
||||||
|
|
||||||
|
await response.StartAsync();
|
||||||
|
|
||||||
|
var memory = response.BodyPipe.GetMemory(4096);
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("Hello ");
|
||||||
|
fisrtPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
|
||||||
|
var secondPartOfResponse = Encoding.ASCII.GetBytes("World!");
|
||||||
|
secondPartOfResponse.CopyTo(memory.Slice(6));
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
}, 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 ChunkGetSpanMultipleAdvance()
|
||||||
|
{
|
||||||
|
var testContext = new TestServiceContext(LoggerFactory);
|
||||||
|
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
var response = httpContext.Response;
|
||||||
|
|
||||||
|
// To avoid using span in an async method
|
||||||
|
void NonAsyncMethod()
|
||||||
|
{
|
||||||
|
var span = response.BodyPipe.GetSpan(4096);
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("Hello ");
|
||||||
|
fisrtPartOfResponse.CopyTo(span);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
|
||||||
|
var secondPartOfResponse = Encoding.ASCII.GetBytes("World!");
|
||||||
|
secondPartOfResponse.CopyTo(span.Slice(6));
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
await response.StartAsync();
|
||||||
|
|
||||||
|
NonAsyncMethod();
|
||||||
|
}, 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 ChunkGetMemoryAndWrite()
|
||||||
|
{
|
||||||
|
var testContext = new TestServiceContext(LoggerFactory);
|
||||||
|
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
var response = httpContext.Response;
|
||||||
|
|
||||||
|
await response.StartAsync();
|
||||||
|
|
||||||
|
var memory = response.BodyPipe.GetMemory(4096);
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("Hello ");
|
||||||
|
fisrtPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.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",
|
||||||
|
"",
|
||||||
|
"c",
|
||||||
|
"Hello World!",
|
||||||
|
"0",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetMemoryWithSizeHint()
|
||||||
|
{
|
||||||
|
var testContext = new TestServiceContext(LoggerFactory);
|
||||||
|
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
var response = httpContext.Response;
|
||||||
|
|
||||||
|
await response.StartAsync();
|
||||||
|
|
||||||
|
var memory = response.BodyPipe.GetMemory(0);
|
||||||
|
|
||||||
|
// Headers are already written to memory, sliced appropriately
|
||||||
|
Assert.Equal(4005, memory.Length);
|
||||||
|
|
||||||
|
memory = response.BodyPipe.GetMemory(1000000);
|
||||||
|
Assert.Equal(4005, memory.Length);
|
||||||
|
}, 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",
|
||||||
|
"",
|
||||||
|
"0",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(15)]
|
||||||
|
[InlineData(255)]
|
||||||
|
public async Task ChunkGetMemoryWithSmallerSizesWork(int writeSize)
|
||||||
|
{
|
||||||
|
var testContext = new TestServiceContext(LoggerFactory);
|
||||||
|
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
var response = httpContext.Response;
|
||||||
|
|
||||||
|
await response.StartAsync();
|
||||||
|
|
||||||
|
var memory = response.BodyPipe.GetMemory(4096);
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes(new string('a', writeSize));
|
||||||
|
fisrtPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(writeSize);
|
||||||
|
}, 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ChunkedWithBothPipeAndStreamWorks()
|
||||||
|
{
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
var response = httpContext.Response;
|
||||||
|
await response.StartAsync();
|
||||||
|
var memory = response.BodyPipe.GetMemory(4096);
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("hello,");
|
||||||
|
fisrtPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
var secondPartOfResponse = Encoding.ASCII.GetBytes(" world");
|
||||||
|
secondPartOfResponse.CopyTo(memory.Slice(6));
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
|
||||||
|
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("hello, world"));
|
||||||
|
await response.BodyPipe.WriteAsync(Encoding.ASCII.GetBytes("hello, world"));
|
||||||
|
await response.WriteAsync("hello, world");
|
||||||
|
|
||||||
|
}, 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",
|
||||||
|
"",
|
||||||
|
"c",
|
||||||
|
"hello, world",
|
||||||
|
"c",
|
||||||
|
"hello, world",
|
||||||
|
"c",
|
||||||
|
"hello, world",
|
||||||
|
"c",
|
||||||
|
"hello, world",
|
||||||
|
"0",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
}
|
||||||
|
await server.StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
@ -18,8 +19,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
{
|
{
|
||||||
public class ConnectionAdapterTests : TestApplicationErrorLoggerLoggedTest
|
public class ConnectionAdapterTests : TestApplicationErrorLoggerLoggedTest
|
||||||
{
|
{
|
||||||
[Fact]
|
|
||||||
public async Task CanReadAndWriteWithRewritingConnectionAdapter()
|
public static TheoryData<RequestDelegate> EchoAppRequestDelegates =>
|
||||||
|
new TheoryData<RequestDelegate>
|
||||||
|
{
|
||||||
|
{ TestApp.EchoApp },
|
||||||
|
{ TestApp.EchoAppPipeWriter }
|
||||||
|
};
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(EchoAppRequestDelegates))]
|
||||||
|
public async Task CanReadAndWriteWithRewritingConnectionAdapter(RequestDelegate requestDelegate)
|
||||||
{
|
{
|
||||||
var adapter = new RewritingConnectionAdapter();
|
var adapter = new RewritingConnectionAdapter();
|
||||||
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
||||||
|
|
@ -31,7 +41,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
|
|
||||||
var sendString = "POST / HTTP/1.0\r\nContent-Length: 12\r\n\r\nHello World?";
|
var sendString = "POST / HTTP/1.0\r\nContent-Length: 12\r\n\r\nHello World?";
|
||||||
|
|
||||||
using (var server = new TestServer(TestApp.EchoApp, serviceContext, listenOptions))
|
using (var server = new TestServer(requestDelegate, serviceContext, listenOptions))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
{
|
{
|
||||||
|
|
@ -50,8 +60,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
Assert.Equal(sendString.Length, adapter.BytesRead);
|
Assert.Equal(sendString.Length, adapter.BytesRead);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public async Task CanReadAndWriteWithAsyncConnectionAdapter()
|
[MemberData(nameof(EchoAppRequestDelegates))]
|
||||||
|
public async Task CanReadAndWriteWithAsyncConnectionAdapter(RequestDelegate requestDelegate)
|
||||||
{
|
{
|
||||||
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
||||||
{
|
{
|
||||||
|
|
@ -60,7 +71,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
|
|
||||||
var serviceContext = new TestServiceContext(LoggerFactory);
|
var serviceContext = new TestServiceContext(LoggerFactory);
|
||||||
|
|
||||||
using (var server = new TestServer(TestApp.EchoApp, serviceContext, listenOptions))
|
using (var server = new TestServer(requestDelegate, serviceContext, listenOptions))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
{
|
{
|
||||||
|
|
@ -80,8 +91,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public async Task ImmediateFinAfterOnConnectionAsyncClosesGracefully()
|
[MemberData(nameof(EchoAppRequestDelegates))]
|
||||||
|
public async Task ImmediateFinAfterOnConnectionAsyncClosesGracefully(RequestDelegate requestDelegate)
|
||||||
{
|
{
|
||||||
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
||||||
{
|
{
|
||||||
|
|
@ -90,7 +102,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
|
|
||||||
var serviceContext = new TestServiceContext(LoggerFactory);
|
var serviceContext = new TestServiceContext(LoggerFactory);
|
||||||
|
|
||||||
using (var server = new TestServer(TestApp.EchoApp, serviceContext, listenOptions))
|
using (var server = new TestServer(requestDelegate, serviceContext, listenOptions))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
{
|
{
|
||||||
|
|
@ -102,8 +114,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public async Task ImmediateFinAfterThrowingClosesGracefully()
|
[MemberData(nameof(EchoAppRequestDelegates))]
|
||||||
|
public async Task ImmediateFinAfterThrowingClosesGracefully(RequestDelegate requestDelegate)
|
||||||
{
|
{
|
||||||
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
||||||
{
|
{
|
||||||
|
|
@ -112,7 +125,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
|
|
||||||
var serviceContext = new TestServiceContext(LoggerFactory);
|
var serviceContext = new TestServiceContext(LoggerFactory);
|
||||||
|
|
||||||
using (var server = new TestServer(TestApp.EchoApp, serviceContext, listenOptions))
|
using (var server = new TestServer(requestDelegate, serviceContext, listenOptions))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
{
|
{
|
||||||
|
|
@ -124,9 +137,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
[CollectDump]
|
[CollectDump]
|
||||||
public async Task ImmediateShutdownAfterOnConnectionAsyncDoesNotCrash()
|
[MemberData(nameof(EchoAppRequestDelegates))]
|
||||||
|
public async Task ImmediateShutdownAfterOnConnectionAsyncDoesNotCrash(RequestDelegate requestDelegate)
|
||||||
{
|
{
|
||||||
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
||||||
{
|
{
|
||||||
|
|
@ -136,7 +150,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
var serviceContext = new TestServiceContext(LoggerFactory);
|
var serviceContext = new TestServiceContext(LoggerFactory);
|
||||||
|
|
||||||
var stopTask = Task.CompletedTask;
|
var stopTask = Task.CompletedTask;
|
||||||
using (var server = new TestServer(TestApp.EchoApp, serviceContext, listenOptions))
|
using (var server = new TestServer(requestDelegate, serviceContext, listenOptions))
|
||||||
using (var shutdownCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
|
using (var shutdownCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
|
|
@ -180,8 +194,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public async Task ThrowingSynchronousConnectionAdapterDoesNotCrashServer()
|
[MemberData(nameof(EchoAppRequestDelegates))]
|
||||||
|
public async Task ThrowingSynchronousConnectionAdapterDoesNotCrashServer(RequestDelegate requestDelegate)
|
||||||
{
|
{
|
||||||
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
||||||
{
|
{
|
||||||
|
|
@ -190,7 +205,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
|
|
||||||
var serviceContext = new TestServiceContext(LoggerFactory);
|
var serviceContext = new TestServiceContext(LoggerFactory);
|
||||||
|
|
||||||
using (var server = new TestServer(TestApp.EchoApp, serviceContext, listenOptions))
|
using (var server = new TestServer(requestDelegate, serviceContext, listenOptions))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
{
|
{
|
||||||
|
|
@ -241,6 +256,40 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanFlushAsyncWithConnectionAdapterPipeWriter()
|
||||||
|
{
|
||||||
|
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
||||||
|
{
|
||||||
|
ConnectionAdapters = { new PassThroughConnectionAdapter() }
|
||||||
|
};
|
||||||
|
|
||||||
|
var serviceContext = new TestServiceContext(LoggerFactory);
|
||||||
|
|
||||||
|
using (var server = new TestServer(async context =>
|
||||||
|
{
|
||||||
|
await context.Response.BodyPipe.WriteAsync(Encoding.ASCII.GetBytes("Hello "));
|
||||||
|
await context.Response.BodyPipe.FlushAsync();
|
||||||
|
await context.Response.BodyPipe.WriteAsync(Encoding.ASCII.GetBytes("World!"));
|
||||||
|
}, serviceContext, listenOptions))
|
||||||
|
{
|
||||||
|
using (var connection = server.CreateConnection())
|
||||||
|
{
|
||||||
|
await connection.Send(
|
||||||
|
"GET / HTTP/1.0",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
await connection.ReceiveEnd(
|
||||||
|
"HTTP/1.1 200 OK",
|
||||||
|
"Connection: close",
|
||||||
|
$"Date: {serviceContext.DateHeaderValue}",
|
||||||
|
"",
|
||||||
|
"Hello World!");
|
||||||
|
}
|
||||||
|
await server.StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class RewritingConnectionAdapter : IConnectionAdapter
|
private class RewritingConnectionAdapter : IConnectionAdapter
|
||||||
{
|
{
|
||||||
private RewritingStream _rewritingStream;
|
private RewritingStream _rewritingStream;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.ExceptionServices;
|
using System.Runtime.ExceptionServices;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Connections;
|
using Microsoft.AspNetCore.Connections;
|
||||||
|
|
@ -228,7 +229,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("/","/")]
|
[InlineData("/", "/")]
|
||||||
[InlineData("/a%5E", "/a^")]
|
[InlineData("/a%5E", "/a^")]
|
||||||
[InlineData("/a%E2%82%AC", "/a€")]
|
[InlineData("/a%E2%82%AC", "/a€")]
|
||||||
[InlineData("/a%2Fb", "/a%2Fb")] // Forward slash, not decoded
|
[InlineData("/a%2Fb", "/a%2Fb")] // Forward slash, not decoded
|
||||||
|
|
@ -910,7 +911,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
Assert.IsType<Http2StreamErrorException>(thrownEx.InnerException);
|
Assert.IsType<Http2StreamErrorException>(thrownEx.InnerException);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact] // TODO https://github.com/aspnet/AspNetCore/issues/7034
|
||||||
public async Task ContentLength_Response_FirstWriteMoreBytesWritten_Throws_Sends500()
|
public async Task ContentLength_Response_FirstWriteMoreBytesWritten_Throws_Sends500()
|
||||||
{
|
{
|
||||||
var headers = new[]
|
var headers = new[]
|
||||||
|
|
@ -928,12 +929,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
await StartStreamAsync(1, headers, endStream: true);
|
await StartStreamAsync(1, headers, endStream: true);
|
||||||
|
|
||||||
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
|
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
|
||||||
withLength: 55,
|
withLength: 56,
|
||||||
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
|
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
|
||||||
withStreamId: 1);
|
withStreamId: 1);
|
||||||
await ExpectAsync(Http2FrameType.DATA,
|
await ExpectAsync(Http2FrameType.RST_STREAM,
|
||||||
withLength: 0,
|
withLength: 4,
|
||||||
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
|
withFlags: (byte)Http2DataFrameFlags.NONE,
|
||||||
withStreamId: 1);
|
withStreamId: 1);
|
||||||
|
|
||||||
Assert.Contains(TestApplicationErrorLogger.Messages, m => m.Exception?.Message.Contains("Response Content-Length mismatch: too many bytes written (12 of 11).") ?? false);
|
Assert.Contains(TestApplicationErrorLogger.Messages, m => m.Exception?.Message.Contains("Response Content-Length mismatch: too many bytes written (12 of 11).") ?? false);
|
||||||
|
|
@ -944,8 +945,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
|
|
||||||
Assert.Equal(3, _decodedHeaders.Count);
|
Assert.Equal(3, _decodedHeaders.Count);
|
||||||
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
|
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
|
||||||
Assert.Equal("500", _decodedHeaders[HeaderNames.Status]);
|
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
|
||||||
Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
|
Assert.Equal("11", _decodedHeaders[HeaderNames.ContentLength]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -2468,5 +2469,719 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
|
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
|
||||||
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
|
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[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(async httpContext =>
|
||||||
|
{
|
||||||
|
var response = httpContext.Response;
|
||||||
|
await response.StartAsync();
|
||||||
|
var memory = response.BodyPipe.GetMemory();
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("hello,");
|
||||||
|
fisrtPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
|
||||||
|
memory = response.BodyPipe.GetMemory();
|
||||||
|
var secondPartOfResponse = Encoding.ASCII.GetBytes(" world");
|
||||||
|
secondPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 WriteAsync_GetMemoryLargeWriteBeforeFirstFlush()
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
await response.StartAsync();
|
||||||
|
|
||||||
|
var memory = response.BodyPipe.GetMemory(5000);
|
||||||
|
Assert.Equal(4096, memory.Length);
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes(new string('a', memory.Length));
|
||||||
|
fisrtPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(memory.Length);
|
||||||
|
|
||||||
|
memory = response.BodyPipe.GetMemory();
|
||||||
|
var secondPartOfResponse = Encoding.ASCII.GetBytes("aaaaaa");
|
||||||
|
secondPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
|
||||||
|
await response.BodyPipe.FlushAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
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: 4102,
|
||||||
|
withFlags: (byte)Http2DataFrameFlags.NONE,
|
||||||
|
withStreamId: 1);
|
||||||
|
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(2, _decodedHeaders.Count);
|
||||||
|
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
|
||||||
|
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
|
||||||
|
Assert.Equal(Encoding.ASCII.GetBytes(new string('a', 4102)), dataFrame.PayloadSequence.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WriteAsync_WithGetMemoryWithInitialFlushWorks()
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
|
await response.BodyPipe.FlushAsync();
|
||||||
|
|
||||||
|
var memory = response.BodyPipe.GetMemory(5000);
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes(new string('a', memory.Length));
|
||||||
|
fisrtPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(memory.Length);
|
||||||
|
|
||||||
|
memory = response.BodyPipe.GetMemory();
|
||||||
|
var secondPartOfResponse = Encoding.ASCII.GetBytes("aaaaaa");
|
||||||
|
secondPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
|
||||||
|
await response.BodyPipe.FlushAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
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: 4102,
|
||||||
|
withFlags: (byte)Http2DataFrameFlags.NONE,
|
||||||
|
withStreamId: 1);
|
||||||
|
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(2, _decodedHeaders.Count);
|
||||||
|
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
|
||||||
|
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
|
||||||
|
Assert.Equal(Encoding.ASCII.GetBytes(new string('a', 4102)), dataFrame.PayloadSequence.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WriteAsync_GetMemoryMultipleAdvance()
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
|
await response.BodyPipe.FlushAsync();
|
||||||
|
|
||||||
|
var memory = response.BodyPipe.GetMemory(4096);
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("hello,");
|
||||||
|
fisrtPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
|
||||||
|
var secondPartOfResponse = Encoding.ASCII.GetBytes(" world");
|
||||||
|
secondPartOfResponse.CopyTo(memory.Slice(6));
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 WriteAsync_GetSpanMultipleAdvance()
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
|
await response.BodyPipe.FlushAsync();
|
||||||
|
|
||||||
|
void NonAsyncMethod()
|
||||||
|
{
|
||||||
|
var span = response.BodyPipe.GetSpan();
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("hello,");
|
||||||
|
fisrtPartOfResponse.CopyTo(span);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
|
||||||
|
var secondPartOfResponse = Encoding.ASCII.GetBytes(" world");
|
||||||
|
secondPartOfResponse.CopyTo(span.Slice(6));
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
}
|
||||||
|
NonAsyncMethod();
|
||||||
|
});
|
||||||
|
|
||||||
|
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 WriteAsync_GetMemoryAndWrite()
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
|
await response.StartAsync();
|
||||||
|
|
||||||
|
var memory = response.BodyPipe.GetMemory(4096);
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("hello,");
|
||||||
|
fisrtPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
|
||||||
|
await response.WriteAsync(" world");
|
||||||
|
});
|
||||||
|
|
||||||
|
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.NONE,
|
||||||
|
withStreamId: 1);
|
||||||
|
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(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 WriteAsync_GetMemoryWithSizeHintAlwaysReturnsSameSize()
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
|
await response.StartAsync();
|
||||||
|
|
||||||
|
var memory = response.BodyPipe.GetMemory(0);
|
||||||
|
Assert.Equal(4096, memory.Length);
|
||||||
|
|
||||||
|
memory = response.BodyPipe.GetMemory(1000000);
|
||||||
|
Assert.Equal(4096, memory.Length);
|
||||||
|
});
|
||||||
|
|
||||||
|
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: 0,
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WriteAsync_BothPipeAndStreamWorks()
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
await response.StartAsync();
|
||||||
|
var memory = response.BodyPipe.GetMemory(4096);
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("hello,");
|
||||||
|
fisrtPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
var secondPartOfResponse = Encoding.ASCII.GetBytes(" world");
|
||||||
|
secondPartOfResponse.CopyTo(memory.Slice(6));
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
|
||||||
|
await response.BodyPipe.FlushAsync();
|
||||||
|
|
||||||
|
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("hello, world"));
|
||||||
|
await response.BodyPipe.WriteAsync(Encoding.ASCII.GetBytes("hello, world"));
|
||||||
|
await response.WriteAsync("hello, world");
|
||||||
|
});
|
||||||
|
|
||||||
|
await StartStreamAsync(1, headers, endStream: true);
|
||||||
|
|
||||||
|
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
|
||||||
|
withLength: 37,
|
||||||
|
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
|
||||||
|
withStreamId: 1);
|
||||||
|
await ExpectAsync(Http2FrameType.DATA,
|
||||||
|
withLength: 12,
|
||||||
|
withFlags: (byte)Http2DataFrameFlags.NONE,
|
||||||
|
withStreamId: 1);
|
||||||
|
await ExpectAsync(Http2FrameType.DATA,
|
||||||
|
withLength: 12,
|
||||||
|
withFlags: (byte)Http2DataFrameFlags.NONE,
|
||||||
|
withStreamId: 1);
|
||||||
|
await ExpectAsync(Http2FrameType.DATA,
|
||||||
|
withLength: 12,
|
||||||
|
withFlags: (byte)Http2DataFrameFlags.NONE,
|
||||||
|
withStreamId: 1);
|
||||||
|
await ExpectAsync(Http2FrameType.DATA,
|
||||||
|
withLength: 12,
|
||||||
|
withFlags: (byte)Http2DataFrameFlags.NONE,
|
||||||
|
withStreamId: 1);
|
||||||
|
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(2, _decodedHeaders.Count);
|
||||||
|
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
|
||||||
|
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ContentLengthWithGetSpanWorks()
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
response.ContentLength = 12;
|
||||||
|
await response.StartAsync();
|
||||||
|
|
||||||
|
void NonAsyncMethod()
|
||||||
|
{
|
||||||
|
var span = response.BodyPipe.GetSpan(4096);
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("hello,");
|
||||||
|
fisrtPartOfResponse.CopyTo(span);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
|
||||||
|
var secondPartOfResponse = Encoding.ASCII.GetBytes(" world");
|
||||||
|
secondPartOfResponse.CopyTo(span.Slice(6));
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
NonAsyncMethod();
|
||||||
|
});
|
||||||
|
|
||||||
|
await StartStreamAsync(1, headers, endStream: true);
|
||||||
|
|
||||||
|
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
|
||||||
|
withLength: 56,
|
||||||
|
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
|
||||||
|
withStreamId: 1);
|
||||||
|
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(3, _decodedHeaders.Count);
|
||||||
|
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
|
||||||
|
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
|
||||||
|
Assert.Equal("12", _decodedHeaders[HeaderNames.ContentLength]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ContentLengthWithGetMemoryWorks()
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
response.ContentLength = 12;
|
||||||
|
await response.StartAsync();
|
||||||
|
|
||||||
|
var memory = response.BodyPipe.GetMemory(4096);
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("Hello ");
|
||||||
|
fisrtPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
|
||||||
|
var secondPartOfResponse = Encoding.ASCII.GetBytes("World!");
|
||||||
|
secondPartOfResponse.CopyTo(memory.Slice(6));
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
await StartStreamAsync(1, headers, endStream: true);
|
||||||
|
|
||||||
|
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
|
||||||
|
withLength: 56,
|
||||||
|
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
|
||||||
|
withStreamId: 1);
|
||||||
|
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(3, _decodedHeaders.Count);
|
||||||
|
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
|
||||||
|
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
|
||||||
|
Assert.Equal("12", _decodedHeaders[HeaderNames.ContentLength]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResponseBodyCanWrite()
|
||||||
|
{
|
||||||
|
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 =>
|
||||||
|
{
|
||||||
|
httpContext.Response.ContentLength = 12;
|
||||||
|
await httpContext.Response.Body.WriteAsync(Encoding.ASCII.GetBytes("hello, world"));
|
||||||
|
});
|
||||||
|
|
||||||
|
await StartStreamAsync(1, headers, endStream: true);
|
||||||
|
|
||||||
|
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
|
||||||
|
withLength: 56,
|
||||||
|
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
|
||||||
|
withStreamId: 1);
|
||||||
|
await ExpectAsync(Http2FrameType.DATA,
|
||||||
|
withLength: 12,
|
||||||
|
withFlags: (byte)Http2DataFrameFlags.NONE,
|
||||||
|
withStreamId: 1);
|
||||||
|
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("12", _decodedHeaders[HeaderNames.ContentLength]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResponseBodyAndResponsePipeWorks()
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
response.ContentLength = 54;
|
||||||
|
await response.StartAsync();
|
||||||
|
var memory = response.BodyPipe.GetMemory(4096);
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("hello,");
|
||||||
|
fisrtPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
var secondPartOfResponse = Encoding.ASCII.GetBytes(" world\r\n");
|
||||||
|
secondPartOfResponse.CopyTo(memory.Slice(6));
|
||||||
|
response.BodyPipe.Advance(8);
|
||||||
|
await response.BodyPipe.FlushAsync();
|
||||||
|
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("hello, world\r\n"));
|
||||||
|
await response.BodyPipe.WriteAsync(Encoding.ASCII.GetBytes("hello, world\r\n"));
|
||||||
|
await response.WriteAsync("hello, world");
|
||||||
|
});
|
||||||
|
|
||||||
|
await StartStreamAsync(1, headers, endStream: true);
|
||||||
|
|
||||||
|
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
|
||||||
|
withLength: 56,
|
||||||
|
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
|
||||||
|
withStreamId: 1);
|
||||||
|
await ExpectAsync(Http2FrameType.DATA,
|
||||||
|
withLength: 14,
|
||||||
|
withFlags: (byte)Http2DataFrameFlags.NONE,
|
||||||
|
withStreamId: 1);
|
||||||
|
await ExpectAsync(Http2FrameType.DATA,
|
||||||
|
withLength: 14,
|
||||||
|
withFlags: (byte)Http2DataFrameFlags.NONE,
|
||||||
|
withStreamId: 1);
|
||||||
|
await ExpectAsync(Http2FrameType.DATA,
|
||||||
|
withLength: 14,
|
||||||
|
withFlags: (byte)Http2DataFrameFlags.NONE,
|
||||||
|
withStreamId: 1);
|
||||||
|
await ExpectAsync(Http2FrameType.DATA,
|
||||||
|
withLength: 12,
|
||||||
|
withFlags: (byte)Http2DataFrameFlags.NONE,
|
||||||
|
withStreamId: 1);
|
||||||
|
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("54", _decodedHeaders[HeaderNames.ContentLength]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResponseBodyPipeCompleteWithoutExceptionDoesNotThrow()
|
||||||
|
{
|
||||||
|
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 context =>
|
||||||
|
{
|
||||||
|
context.Response.BodyPipe.Complete();
|
||||||
|
await Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
await StartStreamAsync(1, headers, endStream: true);
|
||||||
|
|
||||||
|
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
|
||||||
|
withLength: 55,
|
||||||
|
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
|
||||||
|
withStreamId: 1);
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResponseBodyPipeCompleteWithoutExceptionWritesDoNotThrow()
|
||||||
|
{
|
||||||
|
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 context =>
|
||||||
|
{
|
||||||
|
context.Response.BodyPipe.Complete();
|
||||||
|
await context.Response.WriteAsync("");
|
||||||
|
});
|
||||||
|
|
||||||
|
await StartStreamAsync(1, headers, endStream: true);
|
||||||
|
|
||||||
|
// Don't receive content length because we called WriteAsync which caused an invalid response
|
||||||
|
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
|
||||||
|
withLength: 37,
|
||||||
|
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
|
||||||
|
withStreamId: 1);
|
||||||
|
|
||||||
|
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(2, _decodedHeaders.Count);
|
||||||
|
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
|
||||||
|
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResponseBodyPipeCompleteWithExceptionThrows()
|
||||||
|
{
|
||||||
|
var expectedException = new Exception();
|
||||||
|
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 context =>
|
||||||
|
{
|
||||||
|
context.Response.BodyPipe.Complete(expectedException);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
await StartStreamAsync(1, headers, endStream: true);
|
||||||
|
|
||||||
|
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
|
||||||
|
withLength: 55,
|
||||||
|
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
|
||||||
|
withStreamId: 1);
|
||||||
|
|
||||||
|
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("500", _decodedHeaders[HeaderNames.Status]);
|
||||||
|
|
||||||
|
Assert.Contains(TestSink.Writes, w => w.EventId.Id == 13 && w.LogLevel == LogLevel.Error
|
||||||
|
&& w.Exception is ConnectionAbortedException && w.Exception.InnerException == expectedException);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
_mockConnectionContext.Setup(c => c.Abort(It.IsAny<ConnectionAbortedException>())).Callback<ConnectionAbortedException>(ex =>
|
_mockConnectionContext.Setup(c => c.Abort(It.IsAny<ConnectionAbortedException>())).Callback<ConnectionAbortedException>(ex =>
|
||||||
{
|
{
|
||||||
// Emulate transport abort so the _connectionTask completes.
|
// Emulate transport abort so the _connectionTask completes.
|
||||||
_pair.Application.Output.Complete(ex);
|
Task.Run(() => _pair.Application.Output.Complete(ex));
|
||||||
});
|
});
|
||||||
|
|
||||||
_noopApplication = context => Task.CompletedTask;
|
_noopApplication = context => Task.CompletedTask;
|
||||||
|
|
@ -380,7 +380,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
|
|
||||||
_serviceContext = new TestServiceContext(LoggerFactory, _mockKestrelTrace.Object)
|
_serviceContext = new TestServiceContext(LoggerFactory, _mockKestrelTrace.Object)
|
||||||
{
|
{
|
||||||
Scheduler = PipeScheduler.Inline
|
Scheduler = PipeScheduler.Inline,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport;
|
using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
using Microsoft.Extensions.Logging.Testing;
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
|
|
@ -58,6 +59,57 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PipesAreNotPersistedBySettingStreamPipeWriterAcrossRequests()
|
||||||
|
{
|
||||||
|
var responseBodyPersisted = false;
|
||||||
|
PipeWriter bodyPipe = null;
|
||||||
|
using (var server = new TestServer(async context =>
|
||||||
|
{
|
||||||
|
if (context.Response.BodyPipe == bodyPipe)
|
||||||
|
{
|
||||||
|
responseBodyPersisted = true;
|
||||||
|
}
|
||||||
|
bodyPipe = new StreamPipeWriter(new MemoryStream());
|
||||||
|
context.Response.BodyPipe = bodyPipe;
|
||||||
|
|
||||||
|
await context.Response.WriteAsync("hello, world");
|
||||||
|
}, new TestServiceContext(LoggerFactory)))
|
||||||
|
{
|
||||||
|
Assert.Equal(string.Empty, await server.HttpClientSlim.GetStringAsync($"http://localhost:{server.Port}/"));
|
||||||
|
Assert.Equal(string.Empty, await server.HttpClientSlim.GetStringAsync($"http://localhost:{server.Port}/"));
|
||||||
|
|
||||||
|
Assert.False(responseBodyPersisted);
|
||||||
|
|
||||||
|
await server.StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PipesAreNotPersistedAcrossRequests()
|
||||||
|
{
|
||||||
|
var responseBodyPersisted = false;
|
||||||
|
PipeWriter bodyPipe = null;
|
||||||
|
using (var server = new TestServer(async context =>
|
||||||
|
{
|
||||||
|
if (context.Response.BodyPipe == bodyPipe)
|
||||||
|
{
|
||||||
|
responseBodyPersisted = true;
|
||||||
|
}
|
||||||
|
bodyPipe = context.Response.BodyPipe;
|
||||||
|
|
||||||
|
await context.Response.WriteAsync("hello, world");
|
||||||
|
}, new TestServiceContext(LoggerFactory)))
|
||||||
|
{
|
||||||
|
Assert.Equal("hello, world", await server.HttpClientSlim.GetStringAsync($"http://localhost:{server.Port}/"));
|
||||||
|
Assert.Equal("hello, world", await server.HttpClientSlim.GetStringAsync($"http://localhost:{server.Port}/"));
|
||||||
|
|
||||||
|
Assert.False(responseBodyPersisted);
|
||||||
|
|
||||||
|
await server.StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RequestBodyReadAsyncCanBeCancelled()
|
public async Task RequestBodyReadAsyncCanBeCancelled()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
using (var server = new TestServer(async context =>
|
using (var server = new TestServer(async context =>
|
||||||
{
|
{
|
||||||
await context.Response.WriteAsync("hello, world");
|
await context.Response.WriteAsync("hello, world");
|
||||||
await context.Response.Body.FlushAsync();
|
await context.Response.BodyPipe.FlushAsync();
|
||||||
ex = Assert.Throws<InvalidOperationException>(() => context.Response.OnStarting(_ => Task.CompletedTask, null));
|
ex = Assert.Throws<InvalidOperationException>(() => context.Response.OnStarting(_ => Task.CompletedTask, null));
|
||||||
}, new TestServiceContext(LoggerFactory)))
|
}, new TestServiceContext(LoggerFactory)))
|
||||||
{
|
{
|
||||||
|
|
@ -155,14 +155,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
var data = new byte[1024 * 1024 * 10];
|
var data = new byte[1024 * 1024 * 10];
|
||||||
|
|
||||||
var timerTask = Task.Delay(TimeSpan.FromSeconds(1));
|
var timerTask = Task.Delay(TimeSpan.FromSeconds(1));
|
||||||
var writeTask = context.Response.Body.WriteAsync(data, 0, data.Length, cts.Token).DefaultTimeout();
|
var writeTask = context.Response.BodyPipe.WriteAsync(new Memory<byte>(data, 0, data.Length), cts.Token).AsTask().DefaultTimeout();
|
||||||
var completedTask = await Task.WhenAny(writeTask, timerTask);
|
var completedTask = await Task.WhenAny(writeTask, timerTask);
|
||||||
|
|
||||||
while (completedTask == writeTask)
|
while (completedTask == writeTask)
|
||||||
{
|
{
|
||||||
await writeTask;
|
await writeTask;
|
||||||
timerTask = Task.Delay(TimeSpan.FromSeconds(1));
|
timerTask = Task.Delay(TimeSpan.FromSeconds(1));
|
||||||
writeTask = context.Response.Body.WriteAsync(data, 0, data.Length, cts.Token).DefaultTimeout();
|
writeTask = context.Response.BodyPipe.WriteAsync(new Memory<byte>(data, 0, data.Length), cts.Token).AsTask().DefaultTimeout();
|
||||||
completedTask = await Task.WhenAny(writeTask, timerTask);
|
completedTask = await Task.WhenAny(writeTask, timerTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -660,7 +660,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
using (var server = new TestServer(async httpContext =>
|
using (var server = new TestServer(async httpContext =>
|
||||||
{
|
{
|
||||||
await httpContext.Response.WriteAsync(response);
|
await httpContext.Response.WriteAsync(response);
|
||||||
await httpContext.Response.Body.FlushAsync();
|
await httpContext.Response.BodyPipe.FlushAsync();
|
||||||
}, new TestServiceContext(LoggerFactory, mockKestrelTrace.Object)))
|
}, new TestServiceContext(LoggerFactory, mockKestrelTrace.Object)))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
|
|
@ -696,12 +696,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
ServerOptions = { AllowSynchronousIO = true }
|
ServerOptions = { AllowSynchronousIO = true }
|
||||||
};
|
};
|
||||||
|
|
||||||
using (var server = new TestServer(httpContext =>
|
using (var server = new TestServer(async httpContext =>
|
||||||
{
|
{
|
||||||
httpContext.Response.ContentLength = 11;
|
httpContext.Response.ContentLength = 11;
|
||||||
httpContext.Response.Body.Write(Encoding.ASCII.GetBytes("hello,"), 0, 6);
|
await httpContext.Response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("hello,"), 0, 6));
|
||||||
httpContext.Response.Body.Write(Encoding.ASCII.GetBytes(" world"), 0, 6);
|
await httpContext.Response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes(" world"), 0, 6));
|
||||||
return Task.CompletedTask;
|
|
||||||
}, serviceContext))
|
}, serviceContext))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
|
|
@ -774,12 +773,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
ServerOptions = { AllowSynchronousIO = true }
|
ServerOptions = { AllowSynchronousIO = true }
|
||||||
};
|
};
|
||||||
|
|
||||||
using (var server = new TestServer(httpContext =>
|
using (var server = new TestServer(async httpContext =>
|
||||||
{
|
{
|
||||||
var response = Encoding.ASCII.GetBytes("hello, world");
|
var response = Encoding.ASCII.GetBytes("hello, world");
|
||||||
httpContext.Response.ContentLength = 5;
|
httpContext.Response.ContentLength = 5;
|
||||||
httpContext.Response.Body.Write(response, 0, response.Length);
|
await httpContext.Response.BodyPipe.WriteAsync(new Memory<byte>(response, 0, response.Length));
|
||||||
return Task.CompletedTask;
|
|
||||||
}, serviceContext))
|
}, serviceContext))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
|
|
@ -811,11 +809,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
{
|
{
|
||||||
var serviceContext = new TestServiceContext(LoggerFactory);
|
var serviceContext = new TestServiceContext(LoggerFactory);
|
||||||
|
|
||||||
using (var server = new TestServer(httpContext =>
|
using (var server = new TestServer(async httpContext =>
|
||||||
{
|
{
|
||||||
var response = Encoding.ASCII.GetBytes("hello, world");
|
var response = Encoding.ASCII.GetBytes("hello, world");
|
||||||
httpContext.Response.ContentLength = 5;
|
httpContext.Response.ContentLength = 5;
|
||||||
return httpContext.Response.Body.WriteAsync(response, 0, response.Length);
|
await httpContext.Response.BodyPipe.WriteAsync(new Memory<byte>(response, 0, response.Length));
|
||||||
}, serviceContext))
|
}, serviceContext))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
|
|
@ -986,8 +984,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(false)]
|
|
||||||
[InlineData(true)]
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
public async Task WhenAppSetsContentLengthToZeroAndDoesNotWriteNoErrorIsThrown(bool flushResponse)
|
public async Task WhenAppSetsContentLengthToZeroAndDoesNotWriteNoErrorIsThrown(bool flushResponse)
|
||||||
{
|
{
|
||||||
var serviceContext = new TestServiceContext(LoggerFactory);
|
var serviceContext = new TestServiceContext(LoggerFactory);
|
||||||
|
|
@ -998,7 +996,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
|
|
||||||
if (flushResponse)
|
if (flushResponse)
|
||||||
{
|
{
|
||||||
await httpContext.Response.Body.FlushAsync();
|
await httpContext.Response.BodyPipe.FlushAsync();
|
||||||
}
|
}
|
||||||
}, serviceContext))
|
}, serviceContext))
|
||||||
{
|
{
|
||||||
|
|
@ -1165,7 +1163,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
using (var server = new TestServer(async httpContext =>
|
using (var server = new TestServer(async httpContext =>
|
||||||
{
|
{
|
||||||
httpContext.Response.ContentLength = 12;
|
httpContext.Response.ContentLength = 12;
|
||||||
httpContext.Response.Body.Write(Encoding.ASCII.GetBytes("hello, world"), 0, 12);
|
await httpContext.Response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("hello, world"), 0, 12));
|
||||||
await flushed.Task;
|
await flushed.Task;
|
||||||
}, serviceContext))
|
}, serviceContext))
|
||||||
{
|
{
|
||||||
|
|
@ -1408,7 +1406,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
{
|
{
|
||||||
var serviceContext = new TestServiceContext(LoggerFactory) { ServerOptions = { AllowSynchronousIO = true } };
|
var serviceContext = new TestServiceContext(LoggerFactory) { ServerOptions = { AllowSynchronousIO = true } };
|
||||||
|
|
||||||
using (var server = new TestServer(httpContext =>
|
using (var server = new TestServer(async httpContext =>
|
||||||
{
|
{
|
||||||
httpContext.Response.OnStarting(() =>
|
httpContext.Response.OnStarting(() =>
|
||||||
{
|
{
|
||||||
|
|
@ -1421,8 +1419,50 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
httpContext.Response.ContentLength = response.Length - 1;
|
httpContext.Response.ContentLength = response.Length - 1;
|
||||||
|
|
||||||
// If OnStarting is not run before verifying writes, an error response will be sent.
|
// If OnStarting is not run before verifying writes, an error response will be sent.
|
||||||
httpContext.Response.Body.Write(response, 0, response.Length);
|
await httpContext.Response.BodyPipe.WriteAsync(new Memory<byte>(response, 0, response.Length));
|
||||||
return Task.CompletedTask;
|
}, serviceContext))
|
||||||
|
{
|
||||||
|
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",
|
||||||
|
"",
|
||||||
|
"c",
|
||||||
|
"hello, world",
|
||||||
|
"0",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
}
|
||||||
|
await server.StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FirstWriteVerifiedAfterOnStartingWithResponseBody()
|
||||||
|
{
|
||||||
|
var serviceContext = new TestServiceContext(LoggerFactory) { ServerOptions = { AllowSynchronousIO = true } };
|
||||||
|
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
httpContext.Response.OnStarting(() =>
|
||||||
|
{
|
||||||
|
// Change response to chunked
|
||||||
|
httpContext.Response.ContentLength = null;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = Encoding.ASCII.GetBytes("hello, world");
|
||||||
|
httpContext.Response.ContentLength = response.Length - 1;
|
||||||
|
|
||||||
|
// If OnStarting is not run before verifying writes, an error response will be sent.
|
||||||
|
await httpContext.Response.Body.WriteAsync(new Memory<byte>(response, 0, response.Length));
|
||||||
}, serviceContext))
|
}, serviceContext))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
|
|
@ -1452,7 +1492,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
{
|
{
|
||||||
var serviceContext = new TestServiceContext(LoggerFactory) { ServerOptions = { AllowSynchronousIO = true } };
|
var serviceContext = new TestServiceContext(LoggerFactory) { ServerOptions = { AllowSynchronousIO = true } };
|
||||||
|
|
||||||
using (var server = new TestServer(httpContext =>
|
using (var server = new TestServer(async httpContext =>
|
||||||
{
|
{
|
||||||
httpContext.Response.OnStarting(() =>
|
httpContext.Response.OnStarting(() =>
|
||||||
{
|
{
|
||||||
|
|
@ -1465,9 +1505,54 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
httpContext.Response.ContentLength = response.Length - 1;
|
httpContext.Response.ContentLength = response.Length - 1;
|
||||||
|
|
||||||
// If OnStarting is not run before verifying writes, an error response will be sent.
|
// If OnStarting is not run before verifying writes, an error response will be sent.
|
||||||
httpContext.Response.Body.Write(response, 0, response.Length / 2);
|
await httpContext.Response.BodyPipe.WriteAsync(new Memory<byte>(response, 0, response.Length / 2));
|
||||||
httpContext.Response.Body.Write(response, response.Length / 2, response.Length - response.Length / 2);
|
await httpContext.Response.BodyPipe.WriteAsync(new Memory<byte>(response, response.Length / 2, response.Length - response.Length / 2));
|
||||||
return Task.CompletedTask;
|
}, serviceContext))
|
||||||
|
{
|
||||||
|
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",
|
||||||
|
"",
|
||||||
|
"6",
|
||||||
|
"hello,",
|
||||||
|
"6",
|
||||||
|
" world",
|
||||||
|
"0",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
}
|
||||||
|
await server.StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SubsequentWriteVerifiedAfterOnStartingWithResponseBody()
|
||||||
|
{
|
||||||
|
var serviceContext = new TestServiceContext(LoggerFactory) { ServerOptions = { AllowSynchronousIO = true } };
|
||||||
|
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
httpContext.Response.OnStarting(() =>
|
||||||
|
{
|
||||||
|
// Change response to chunked
|
||||||
|
httpContext.Response.ContentLength = null;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = Encoding.ASCII.GetBytes("hello, world");
|
||||||
|
httpContext.Response.ContentLength = response.Length - 1;
|
||||||
|
|
||||||
|
// If OnStarting is not run before verifying writes, an error response will be sent.
|
||||||
|
await httpContext.Response.Body.WriteAsync(new Memory<byte>(response, 0, response.Length / 2));
|
||||||
|
await httpContext.Response.Body.WriteAsync(new Memory<byte>(response, response.Length / 2, response.Length - response.Length / 2));
|
||||||
}, serviceContext))
|
}, serviceContext))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
|
|
@ -1510,7 +1595,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
httpContext.Response.ContentLength = response.Length - 1;
|
httpContext.Response.ContentLength = response.Length - 1;
|
||||||
|
|
||||||
// If OnStarting is not run before verifying writes, an error response will be sent.
|
// If OnStarting is not run before verifying writes, an error response will be sent.
|
||||||
return httpContext.Response.Body.WriteAsync(response, 0, response.Length);
|
return httpContext.Response.BodyPipe.WriteAsync(new Memory<byte>(response, 0, response.Length)).AsTask();
|
||||||
}, new TestServiceContext(LoggerFactory)))
|
}, new TestServiceContext(LoggerFactory)))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
|
|
@ -1551,8 +1636,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
httpContext.Response.ContentLength = response.Length - 1;
|
httpContext.Response.ContentLength = response.Length - 1;
|
||||||
|
|
||||||
// If OnStarting is not run before verifying writes, an error response will be sent.
|
// If OnStarting is not run before verifying writes, an error response will be sent.
|
||||||
await httpContext.Response.Body.WriteAsync(response, 0, response.Length / 2);
|
await httpContext.Response.BodyPipe.WriteAsync(new Memory<byte>(response, 0, response.Length / 2));
|
||||||
await httpContext.Response.Body.WriteAsync(response, response.Length / 2, response.Length - response.Length / 2);
|
await httpContext.Response.BodyPipe.WriteAsync(new Memory<byte>(response, response.Length / 2, response.Length - response.Length / 2));
|
||||||
}, new TestServiceContext(LoggerFactory)))
|
}, new TestServiceContext(LoggerFactory)))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
|
|
@ -2112,7 +2197,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
throw onStartingException;
|
throw onStartingException;
|
||||||
}, null);
|
}, null);
|
||||||
|
|
||||||
var writeException = await Assert.ThrowsAsync<ObjectDisposedException>(async () => await response.Body.FlushAsync());
|
var writeException = await Assert.ThrowsAsync<ObjectDisposedException>(async () => await response.BodyPipe.FlushAsync());
|
||||||
Assert.Same(onStartingException, writeException.InnerException);
|
Assert.Same(onStartingException, writeException.InnerException);
|
||||||
}, testContext))
|
}, testContext))
|
||||||
{
|
{
|
||||||
|
|
@ -2171,7 +2256,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
|
|
||||||
response.Headers["Content-Length"] = new[] { "11" };
|
response.Headers["Content-Length"] = new[] { "11" };
|
||||||
|
|
||||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
|
await response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("Hello World"), 0, 11));
|
||||||
}, testContext))
|
}, testContext))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
|
|
@ -2218,7 +2303,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
|
|
||||||
response.Headers["Content-Length"] = new[] { "11" };
|
response.Headers["Content-Length"] = new[] { "11" };
|
||||||
|
|
||||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
|
await response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("Hello World"), 0, 11));
|
||||||
}, testContext))
|
}, testContext))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
|
|
@ -2264,7 +2349,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
|
|
||||||
response.Headers["Content-Length"] = new[] { "11" };
|
response.Headers["Content-Length"] = new[] { "11" };
|
||||||
|
|
||||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
|
await response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("Hello World"), 0, 11));
|
||||||
}, testContext))
|
}, testContext))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
|
|
@ -2307,7 +2392,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
}, null);
|
}, null);
|
||||||
|
|
||||||
response.Headers["Content-Length"] = new[] { "11" };
|
response.Headers["Content-Length"] = new[] { "11" };
|
||||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
|
await response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("Hello World"), 0, 11));
|
||||||
throw new Exception();
|
throw new Exception();
|
||||||
}, testContext))
|
}, testContext))
|
||||||
{
|
{
|
||||||
|
|
@ -2349,7 +2434,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
}, null);
|
}, null);
|
||||||
|
|
||||||
response.Headers["Content-Length"] = new[] { "11" };
|
response.Headers["Content-Length"] = new[] { "11" };
|
||||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello"), 0, 5);
|
await response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("Hello"), 0, 5));
|
||||||
throw new Exception();
|
throw new Exception();
|
||||||
}, testContext))
|
}, testContext))
|
||||||
{
|
{
|
||||||
|
|
@ -2384,7 +2469,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
{
|
{
|
||||||
var response = httpContext.Response;
|
var response = httpContext.Response;
|
||||||
response.Headers["Content-Length"] = new[] { "11" };
|
response.Headers["Content-Length"] = new[] { "11" };
|
||||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
|
await response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("Hello World"), 0, 11));
|
||||||
}, testContext))
|
}, testContext))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
|
|
@ -2829,7 +2914,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
using (var server = new TestServer(async httpContext =>
|
using (var server = new TestServer(async httpContext =>
|
||||||
{
|
{
|
||||||
await httpContext.Response.StartAsync();
|
await httpContext.Response.StartAsync();
|
||||||
await httpContext.Response.Body.FlushAsync();
|
await httpContext.Response.BodyPipe.FlushAsync();
|
||||||
Assert.True(httpContext.Response.HasStarted);
|
Assert.True(httpContext.Response.HasStarted);
|
||||||
}, testContext))
|
}, testContext))
|
||||||
{
|
{
|
||||||
|
|
@ -2983,7 +3068,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
var ioEx = Assert.Throws<InvalidOperationException>(() => context.Response.Body.Write(Encoding.ASCII.GetBytes("What!?"), 0, 6));
|
var ioEx = Assert.Throws<InvalidOperationException>(() => context.Response.Body.Write(Encoding.ASCII.GetBytes("What!?"), 0, 6));
|
||||||
Assert.Equal(CoreStrings.SynchronousWritesDisallowed, ioEx.Message);
|
Assert.Equal(CoreStrings.SynchronousWritesDisallowed, ioEx.Message);
|
||||||
|
|
||||||
await context.Response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello2"), 0, 6);
|
await context.Response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("Hello2"), 0, 6));
|
||||||
}
|
}
|
||||||
}, new TestServiceContext(LoggerFactory)))
|
}, new TestServiceContext(LoggerFactory)))
|
||||||
{
|
{
|
||||||
|
|
@ -3028,7 +3113,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
var ioEx = Assert.Throws<InvalidOperationException>(() => context.Response.Body.Write(Encoding.ASCII.GetBytes("What!?"), 0, 6));
|
var ioEx = Assert.Throws<InvalidOperationException>(() => context.Response.Body.Write(Encoding.ASCII.GetBytes("What!?"), 0, 6));
|
||||||
Assert.Equal(CoreStrings.SynchronousWritesDisallowed, ioEx.Message);
|
Assert.Equal(CoreStrings.SynchronousWritesDisallowed, ioEx.Message);
|
||||||
|
|
||||||
return context.Response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello!"), 0, 6);
|
return context.Response.BodyPipe.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("Hello!"), 0, 6)).AsTask();
|
||||||
}, testContext))
|
}, testContext))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
|
|
@ -3079,6 +3164,662 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AdvanceNegativeValueThrowsArgumentOutOfRangeException()
|
||||||
|
{
|
||||||
|
var testContext = new TestServiceContext(LoggerFactory);
|
||||||
|
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
var response = httpContext.Response;
|
||||||
|
|
||||||
|
await response.StartAsync();
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => response.BodyPipe.Advance(-1));
|
||||||
|
}, 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",
|
||||||
|
"",
|
||||||
|
"0",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AdvanceWithTooLargeOfAValueThrowInvalidOperationException()
|
||||||
|
{
|
||||||
|
var testContext = new TestServiceContext(LoggerFactory);
|
||||||
|
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
var response = httpContext.Response;
|
||||||
|
|
||||||
|
await response.StartAsync();
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => response.BodyPipe.Advance(1));
|
||||||
|
}, 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",
|
||||||
|
"",
|
||||||
|
"0",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetMemoryBeforeStartAsyncThrows()
|
||||||
|
{
|
||||||
|
var testContext = new TestServiceContext(LoggerFactory);
|
||||||
|
|
||||||
|
using (var server = new TestServer(httpContext =>
|
||||||
|
{
|
||||||
|
Assert.Throws<InvalidOperationException>(() => httpContext.Response.BodyPipe.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.BodyPipe.GetSpan(4096);
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("Hello ");
|
||||||
|
fisrtPartOfResponse.CopyTo(span);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
|
||||||
|
var secondPartOfResponse = Encoding.ASCII.GetBytes("World!");
|
||||||
|
secondPartOfResponse.CopyTo(span.Slice(6));
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
NonAsyncMethod();
|
||||||
|
|
||||||
|
}, 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: 12",
|
||||||
|
"",
|
||||||
|
"Hello World!");
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ContentLengthWithGetMemoryWorks()
|
||||||
|
{
|
||||||
|
var testContext = new TestServiceContext(LoggerFactory);
|
||||||
|
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
var response = httpContext.Response;
|
||||||
|
response.ContentLength = 12;
|
||||||
|
await response.StartAsync();
|
||||||
|
|
||||||
|
var memory = response.BodyPipe.GetMemory(4096);
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("Hello ");
|
||||||
|
fisrtPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
|
||||||
|
var secondPartOfResponse = Encoding.ASCII.GetBytes("World!");
|
||||||
|
secondPartOfResponse.CopyTo(memory.Slice(6));
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
}, 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: 12",
|
||||||
|
"",
|
||||||
|
"Hello World!");
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResponseBodyCanWrite()
|
||||||
|
{
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
httpContext.Response.ContentLength = 12;
|
||||||
|
await httpContext.Response.Body.WriteAsync(Encoding.ASCII.GetBytes("hello, world"));
|
||||||
|
}, 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: 12",
|
||||||
|
"",
|
||||||
|
"hello, world");
|
||||||
|
}
|
||||||
|
await server.StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResponseBodyAndResponsePipeWorks()
|
||||||
|
{
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
var response = httpContext.Response;
|
||||||
|
response.ContentLength = 54;
|
||||||
|
await response.StartAsync();
|
||||||
|
var memory = response.BodyPipe.GetMemory(4096);
|
||||||
|
var fisrtPartOfResponse = Encoding.ASCII.GetBytes("hello,");
|
||||||
|
fisrtPartOfResponse.CopyTo(memory);
|
||||||
|
response.BodyPipe.Advance(6);
|
||||||
|
var secondPartOfResponse = Encoding.ASCII.GetBytes(" world\r\n");
|
||||||
|
secondPartOfResponse.CopyTo(memory.Slice(6));
|
||||||
|
response.BodyPipe.Advance(8);
|
||||||
|
|
||||||
|
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("hello, world\r\n"));
|
||||||
|
await response.BodyPipe.WriteAsync(Encoding.ASCII.GetBytes("hello, world\r\n"));
|
||||||
|
await response.WriteAsync("hello, world");
|
||||||
|
|
||||||
|
}, 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: 54",
|
||||||
|
"",
|
||||||
|
"hello, world",
|
||||||
|
"hello, world",
|
||||||
|
"hello, world",
|
||||||
|
"hello, world");
|
||||||
|
}
|
||||||
|
await server.StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResponseBodyPipeCompleteWithoutExceptionDoesNotThrow()
|
||||||
|
{
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
httpContext.Response.BodyPipe.Complete();
|
||||||
|
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 ResponseBodyPipeCompleteWithoutExceptionWritesDoNotThrow()
|
||||||
|
{
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
httpContext.Response.BodyPipe.Complete();
|
||||||
|
await httpContext.Response.WriteAsync("test");
|
||||||
|
}, 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 ResponsePipeWriterCompleteWithException()
|
||||||
|
{
|
||||||
|
var expectedException = new Exception();
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
httpContext.Response.BodyPipe.Complete(expectedException);
|
||||||
|
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 500 Internal Server Error",
|
||||||
|
$"Date: {server.Context.DateHeaderValue}",
|
||||||
|
"Content-Length: 0",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
Assert.Contains(TestSink.Writes, w => w.EventId.Id == 13 && w.LogLevel == LogLevel.Error
|
||||||
|
&& w.Exception is ConnectionAbortedException && w.Exception.InnerException == expectedException);
|
||||||
|
}
|
||||||
|
await server.StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResponseCompleteGetMemoryReturnsRentedMemory()
|
||||||
|
{
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
await httpContext.Response.StartAsync();
|
||||||
|
httpContext.Response.BodyPipe.Complete();
|
||||||
|
var memory = httpContext.Response.BodyPipe.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}",
|
||||||
|
"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.BodyPipe.Complete();
|
||||||
|
for (var i = 0; i < 5; i++)
|
||||||
|
{
|
||||||
|
var memory = httpContext.Response.BodyPipe.GetMemory(); // Shouldn't throw
|
||||||
|
httpContext.Response.BodyPipe.Advance(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}",
|
||||||
|
"Transfer-Encoding: chunked",
|
||||||
|
"",
|
||||||
|
"0",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
}
|
||||||
|
await server.StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResponseSetBodyAndPipeBodyIsWrapped()
|
||||||
|
{
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
httpContext.Response.Body = new MemoryStream();
|
||||||
|
httpContext.Response.BodyPipe = new Pipe().Writer;
|
||||||
|
Assert.IsType<WriteOnlyPipeStream>(httpContext.Response.Body);
|
||||||
|
|
||||||
|
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 ResponseSetBodyToSameValueTwiceGetPipeMultipleTimesDifferentObject()
|
||||||
|
{
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
var memoryStream = new MemoryStream();
|
||||||
|
httpContext.Response.Body = memoryStream;
|
||||||
|
var bodyPipe1 = httpContext.Response.BodyPipe;
|
||||||
|
|
||||||
|
httpContext.Response.Body = memoryStream;
|
||||||
|
var bodyPipe2 = httpContext.Response.BodyPipe;
|
||||||
|
|
||||||
|
Assert.NotEqual(bodyPipe1, bodyPipe2);
|
||||||
|
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 ResponseSetPipeToSameValueTwiceGetBodyMultipleTimesDifferent()
|
||||||
|
{
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
var pipeWriter = new Pipe().Writer;
|
||||||
|
httpContext.Response.BodyPipe = pipeWriter;
|
||||||
|
var body1 = httpContext.Response.Body;
|
||||||
|
|
||||||
|
httpContext.Response.BodyPipe = pipeWriter;
|
||||||
|
var body2 = httpContext.Response.Body;
|
||||||
|
|
||||||
|
Assert.NotEqual(body1, body2);
|
||||||
|
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 ResponseSetPipeAndBodyPipeIsWrapped()
|
||||||
|
{
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
httpContext.Response.BodyPipe = new Pipe().Writer;
|
||||||
|
httpContext.Response.Body = new MemoryStream();
|
||||||
|
Assert.IsType<StreamPipeWriter>(httpContext.Response.BodyPipe);
|
||||||
|
Assert.IsType<MemoryStream>(httpContext.Response.Body);
|
||||||
|
|
||||||
|
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 ResponseWriteToBodyPipeAndStreamAllBlocksDisposed()
|
||||||
|
{
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
httpContext.Response.BodyPipe = new Pipe().Writer;
|
||||||
|
await httpContext.Response.Body.WriteAsync(new byte[1]);
|
||||||
|
httpContext.Response.Body = new MemoryStream();
|
||||||
|
await httpContext.Response.BodyPipe.WriteAsync(new byte[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMemoryPool will confirm that all rented blocks have been disposed, meaning dispose was called.
|
||||||
|
|
||||||
|
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 ResponseStreamWrappingWorks()
|
||||||
|
{
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
var oldBody = httpContext.Response.Body;
|
||||||
|
httpContext.Response.Body = new MemoryStream();
|
||||||
|
|
||||||
|
await httpContext.Response.BodyPipe.WriteAsync(new byte[1]);
|
||||||
|
await httpContext.Response.Body.WriteAsync(new byte[1]);
|
||||||
|
|
||||||
|
Assert.Equal(2, httpContext.Response.Body.Length);
|
||||||
|
|
||||||
|
httpContext.Response.Body = oldBody;
|
||||||
|
|
||||||
|
// Even though we are restoring the original response body, we will create a
|
||||||
|
// wrapper rather than restoring the original pipe.
|
||||||
|
Assert.IsType<StreamPipeWriter>(httpContext.Response.BodyPipe);
|
||||||
|
|
||||||
|
}, 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 ResponsePipeWrappingWorks()
|
||||||
|
{
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
var oldPipeWriter = httpContext.Response.BodyPipe;
|
||||||
|
var pipe = new Pipe();
|
||||||
|
httpContext.Response.BodyPipe = pipe.Writer;
|
||||||
|
|
||||||
|
await httpContext.Response.Body.WriteAsync(new byte[1]);
|
||||||
|
await httpContext.Response.BodyPipe.WriteAsync(new byte[1]);
|
||||||
|
|
||||||
|
var readResult = await pipe.Reader.ReadAsync();
|
||||||
|
Assert.Equal(2, readResult.Buffer.Length);
|
||||||
|
|
||||||
|
httpContext.Response.BodyPipe = oldPipeWriter;
|
||||||
|
|
||||||
|
// Even though we are restoring the original response body, we will create a
|
||||||
|
// wrapper rather than restoring the original pipe.
|
||||||
|
Assert.IsType<WriteOnlyPipeStream>(httpContext.Response.Body);
|
||||||
|
}, 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task ResponseStatusCodeSetBeforeHttpContextDispose(
|
private static async Task ResponseStatusCodeSetBeforeHttpContextDispose(
|
||||||
ITestSink testSink,
|
ITestSink testSink,
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ namespace CodeGenerator
|
||||||
{
|
{
|
||||||
"IHttpRequestFeature",
|
"IHttpRequestFeature",
|
||||||
"IHttpResponseFeature",
|
"IHttpResponseFeature",
|
||||||
|
"IResponseBodyPipeFeature",
|
||||||
"IHttpRequestIdentifierFeature",
|
"IHttpRequestIdentifierFeature",
|
||||||
"IServiceProvidersFeature",
|
"IServiceProvidersFeature",
|
||||||
"IHttpRequestLifetimeFeature",
|
"IHttpRequestLifetimeFeature",
|
||||||
|
|
@ -60,6 +61,7 @@ namespace CodeGenerator
|
||||||
{
|
{
|
||||||
"IHttpRequestFeature",
|
"IHttpRequestFeature",
|
||||||
"IHttpResponseFeature",
|
"IHttpResponseFeature",
|
||||||
|
"IResponseBodyPipeFeature",
|
||||||
"IHttpUpgradeFeature",
|
"IHttpUpgradeFeature",
|
||||||
"IHttpRequestIdentifierFeature",
|
"IHttpRequestIdentifierFeature",
|
||||||
"IHttpRequestLifetimeFeature",
|
"IHttpRequestLifetimeFeature",
|
||||||
|
|
@ -68,7 +70,7 @@ namespace CodeGenerator
|
||||||
"IHttpBodyControlFeature",
|
"IHttpBodyControlFeature",
|
||||||
"IHttpResponseStartFeature"
|
"IHttpResponseStartFeature"
|
||||||
};
|
};
|
||||||
|
|
||||||
var usings = $@"
|
var usings = $@"
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Http.Features.Authentication;
|
using Microsoft.AspNetCore.Http.Features.Authentication;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue