Expose HttpResponse PipeWriter in Kestrel (#7110)

This commit is contained in:
Justin Kotalik 2019-02-08 17:24:26 -08:00 committed by GitHub
parent 7b3149af1e
commit 35b99e44ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 3291 additions and 522 deletions

View File

@ -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);
}
} }
} }
} }

View File

@ -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();

View File

@ -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>

View File

@ -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);
} }
} }

View File

@ -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

View File

@ -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);
} }
} }
} }

View File

@ -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))
{ {

View File

@ -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);
} }

View File

@ -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();
} }
} }
} }

View File

@ -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()
{ {

View File

@ -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]);

View File

@ -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();

View File

@ -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);

View File

@ -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);
}
} }
} }

View File

@ -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;
}
} }
} }

View File

@ -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);
}
} }
} }
} }

View File

@ -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);

View File

@ -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);
} }

View File

@ -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;

View File

@ -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>

View File

@ -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)

View File

@ -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;

View File

@ -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;
}
} }
} }

View File

@ -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);
} }
} }

View File

@ -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);

View File

@ -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);
}
}
} }
} }

View File

@ -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);
}
}
}
}

View File

@ -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);
}
} }
} }
} }

View File

@ -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;

View File

@ -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();
} }
} }

View File

@ -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);
} }
} }

View File

@ -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();
}
}

View File

@ -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()

View File

@ -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;

View File

@ -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)

View File

@ -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);
} }

View File

@ -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;
} }
} }
} }

View File

@ -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)
{ {

View File

@ -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)]);

View File

@ -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>());
}
}
}

View File

@ -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);
}
} }
} }

View File

@ -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" />

View File

@ -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;
} }

View File

@ -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");

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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();
}
} }
} }

View File

@ -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))

View File

@ -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();
}
}
} }
} }

View File

@ -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;

View File

@ -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);
}
} }
} }

View File

@ -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,
}; };
} }

View File

@ -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()
{ {

View File

@ -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,

View File

@ -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;