Implement HTTP/2 output flow control (#2690)
This commit is contained in:
parent
6c9d9f2b0c
commit
5e4fae2573
|
|
@ -524,4 +524,22 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
|
||||||
<data name="Http2ErrorInvalidPreface" xml:space="preserve">
|
<data name="Http2ErrorInvalidPreface" xml:space="preserve">
|
||||||
<value>Invalid HTTP/2 connection preface.</value>
|
<value>Invalid HTTP/2 connection preface.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ConnectionOrStreamAbortedByCancellationToken" xml:space="preserve">
|
||||||
|
<value>The connection or stream was aborted because a write operation was aborted with a CancellationToken.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Http2ErrorInitialWindowSizeInvalid" xml:space="preserve">
|
||||||
|
<value>The client sent a SETTINGS frame with a SETTINGS_INITIAL_WINDOW_SIZE that caused a flow-control window to exceed the maximum size.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Http2ErrorWindowUpdateSizeInvalid" xml:space="preserve">
|
||||||
|
<value>The client sent a WINDOW_UPDATE frame that caused a flow-control window to exceed the maximum size.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Http2ConnectionFaulted" xml:space="preserve">
|
||||||
|
<value>The HTTP/2 connection faulted.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Http2StreamResetByClient" xml:space="preserve">
|
||||||
|
<value>The client reset the request stream.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Http2StreamAborted" xml:space="preserve">
|
||||||
|
<value>The request stream was aborted.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -4,12 +4,10 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Text;
|
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;
|
||||||
using Microsoft.AspNetCore.Connections.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;
|
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
|
||||||
|
|
||||||
|
|
@ -27,6 +25,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
private readonly ITimeoutControl _timeoutControl;
|
private readonly ITimeoutControl _timeoutControl;
|
||||||
private readonly IKestrelTrace _log;
|
private readonly IKestrelTrace _log;
|
||||||
private readonly IBytesWrittenFeature _transportBytesWrittenFeature;
|
private readonly IBytesWrittenFeature _transportBytesWrittenFeature;
|
||||||
|
private readonly StreamSafePipeFlusher _flusher;
|
||||||
|
|
||||||
// This locks access to to all of the below fields
|
// This locks access to to all of the below fields
|
||||||
private readonly object _contextLock = new object();
|
private readonly object _contextLock = new object();
|
||||||
|
|
@ -37,16 +36,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
private long _totalBytesCommitted;
|
private long _totalBytesCommitted;
|
||||||
|
|
||||||
private readonly PipeWriter _pipeWriter;
|
private readonly PipeWriter _pipeWriter;
|
||||||
|
|
||||||
// https://github.com/dotnet/corefxlab/issues/1334
|
|
||||||
// Pipelines don't support multiple awaiters on flush
|
|
||||||
// this is temporary until it does
|
|
||||||
private TaskCompletionSource<object> _flushTcs;
|
|
||||||
private readonly object _flushLock = new object();
|
|
||||||
private Action _flushCompleted;
|
|
||||||
|
|
||||||
private ValueTask<FlushResult> _flushTask;
|
|
||||||
|
|
||||||
public Http1OutputProducer(
|
public Http1OutputProducer(
|
||||||
PipeWriter pipeWriter,
|
PipeWriter pipeWriter,
|
||||||
string connectionId,
|
string connectionId,
|
||||||
|
|
@ -60,11 +49,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
_connectionContext = connectionContext;
|
_connectionContext = connectionContext;
|
||||||
_timeoutControl = timeoutControl;
|
_timeoutControl = timeoutControl;
|
||||||
_log = log;
|
_log = log;
|
||||||
_flushCompleted = OnFlushCompleted;
|
|
||||||
_transportBytesWrittenFeature = transportBytesWrittenFeature;
|
_transportBytesWrittenFeature = transportBytesWrittenFeature;
|
||||||
|
_flusher = new StreamSafePipeFlusher(pipeWriter, timeoutControl);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteDataAsync(ReadOnlySpan<byte> buffer, CancellationToken cancellationToken = default(CancellationToken))
|
public Task WriteDataAsync(ReadOnlySpan<byte> buffer, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
|
|
@ -74,12 +63,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
return WriteAsync(buffer, cancellationToken);
|
return WriteAsync(buffer, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteStreamSuffixAsync(CancellationToken cancellationToken)
|
public Task WriteStreamSuffixAsync()
|
||||||
{
|
{
|
||||||
return WriteAsync(_endChunkedResponseBytes.Span, cancellationToken);
|
return WriteAsync(_endChunkedResponseBytes.Span);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
|
public Task FlushAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return WriteAsync(Constants.EmptyData, cancellationToken);
|
return WriteAsync(Constants.EmptyData, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
@ -191,17 +180,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Write100ContinueAsync(CancellationToken cancellationToken)
|
public Task Write100ContinueAsync()
|
||||||
{
|
{
|
||||||
return WriteAsync(_continueBytes.Span, default(CancellationToken));
|
return WriteAsync(_continueBytes.Span);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task WriteAsync(
|
private Task WriteAsync(
|
||||||
ReadOnlySpan<byte> buffer,
|
ReadOnlySpan<byte> buffer,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var writableBuffer = default(PipeWriter);
|
|
||||||
long bytesWritten = 0;
|
|
||||||
lock (_contextLock)
|
lock (_contextLock)
|
||||||
{
|
{
|
||||||
if (_completed)
|
if (_completed)
|
||||||
|
|
@ -209,8 +196,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
writableBuffer = _pipeWriter;
|
var writer = new CountingBufferWriter<PipeWriter>(_pipeWriter);
|
||||||
var writer = new CountingBufferWriter<PipeWriter>(writableBuffer);
|
|
||||||
if (buffer.Length > 0)
|
if (buffer.Length > 0)
|
||||||
{
|
{
|
||||||
writer.Write(buffer);
|
writer.Write(buffer);
|
||||||
|
|
@ -220,74 +206,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
}
|
}
|
||||||
writer.Commit();
|
writer.Commit();
|
||||||
|
|
||||||
bytesWritten = _unflushedBytes;
|
var bytesWritten = _unflushedBytes;
|
||||||
_unflushedBytes = 0;
|
_unflushedBytes = 0;
|
||||||
}
|
|
||||||
|
|
||||||
return FlushAsync(writableBuffer, bytesWritten, cancellationToken);
|
return _flusher.FlushAsync(bytesWritten, this, cancellationToken);
|
||||||
}
|
|
||||||
|
|
||||||
// Single caller, at end of method - so inline
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private Task FlushAsync(PipeWriter writableBuffer, long bytesWritten, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var awaitable = writableBuffer.FlushAsync(cancellationToken);
|
|
||||||
if (awaitable.IsCompleted)
|
|
||||||
{
|
|
||||||
// The flush task can't fail today
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
return FlushAsyncAwaited(awaitable, bytesWritten, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task FlushAsyncAwaited(ValueTask<FlushResult> awaitable, long count, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// https://github.com/dotnet/corefxlab/issues/1334
|
|
||||||
// Since the flush awaitable doesn't currently support multiple awaiters
|
|
||||||
// we need to use a task to track the callbacks.
|
|
||||||
// All awaiters get the same task
|
|
||||||
lock (_flushLock)
|
|
||||||
{
|
|
||||||
_flushTask = awaitable;
|
|
||||||
if (_flushTcs == null || _flushTcs.Task.IsCompleted)
|
|
||||||
{
|
|
||||||
_flushTcs = new TaskCompletionSource<object>();
|
|
||||||
|
|
||||||
_flushTask.GetAwaiter().OnCompleted(_flushCompleted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_timeoutControl.StartTimingWrite(count);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _flushTcs.Task;
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
_completed = true;
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_timeoutControl.StopTimingWrite();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnFlushCompleted()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_flushTask.GetAwaiter().GetResult();
|
|
||||||
_flushTcs.TrySetResult(null);
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
_flushTcs.TrySetResult(exception);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_flushTask = default;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -429,7 +429,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Immediately kill the connection and poison the request and response streams with an error if there is one.
|
/// Immediately kill the connection and poison the request and response streams with an error if there is one.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Abort(ConnectionAbortedException abortReason)
|
public virtual void Abort(ConnectionAbortedException abortReason)
|
||||||
{
|
{
|
||||||
if (Interlocked.Exchange(ref _requestAborted, 1) != 0)
|
if (Interlocked.Exchange(ref _requestAborted, 1) != 0)
|
||||||
{
|
{
|
||||||
|
|
@ -966,7 +966,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
RequestHeaders.TryGetValue("Expect", out var expect) &&
|
RequestHeaders.TryGetValue("Expect", out var expect) &&
|
||||||
(expect.FirstOrDefault() ?? "").Equals("100-continue", StringComparison.OrdinalIgnoreCase))
|
(expect.FirstOrDefault() ?? "").Equals("100-continue", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
Output.Write100ContinueAsync(default(CancellationToken)).GetAwaiter().GetResult();
|
Output.Write100ContinueAsync().GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1097,7 +1097,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
// For the same reason we call CheckLastWrite() in Content-Length responses.
|
// For the same reason we call CheckLastWrite() in Content-Length responses.
|
||||||
_abortedCts = null;
|
_abortedCts = null;
|
||||||
|
|
||||||
await Output.WriteStreamSuffixAsync(default(CancellationToken));
|
await Output.WriteStreamSuffixAsync();
|
||||||
|
|
||||||
if (_keepAlive)
|
if (_keepAlive)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
void Abort(ConnectionAbortedException abortReason);
|
void Abort(ConnectionAbortedException abortReason);
|
||||||
Task WriteAsync<T>(Func<PipeWriter, T, long> callback, T state);
|
Task WriteAsync<T>(Func<PipeWriter, T, long> callback, T state);
|
||||||
Task FlushAsync(CancellationToken cancellationToken);
|
Task FlushAsync(CancellationToken cancellationToken);
|
||||||
Task Write100ContinueAsync(CancellationToken cancellationToken);
|
Task Write100ContinueAsync();
|
||||||
void WriteResponseHeaders(int statusCode, string ReasonPhrase, HttpResponseHeaders responseHeaders);
|
void WriteResponseHeaders(int statusCode, string ReasonPhrase, HttpResponseHeaders responseHeaders);
|
||||||
// The reason this is ReadOnlySpan and not ReadOnlyMemory is because writes are always
|
// The reason this is ReadOnlySpan and not ReadOnlyMemory is because writes are always
|
||||||
// synchronous. Flushing to get back pressure is the only time we truly go async but
|
// synchronous. Flushing to get back pressure is the only time we truly go async but
|
||||||
// that's after the buffer is copied
|
// that's after the buffer is copied
|
||||||
Task WriteDataAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken);
|
Task WriteDataAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken);
|
||||||
Task WriteStreamSuffixAsync(CancellationToken cancellationToken);
|
Task WriteStreamSuffixAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
private readonly Http2ConnectionContext _context;
|
private readonly Http2ConnectionContext _context;
|
||||||
private readonly Http2FrameWriter _frameWriter;
|
private readonly Http2FrameWriter _frameWriter;
|
||||||
private readonly HPackDecoder _hpackDecoder;
|
private readonly HPackDecoder _hpackDecoder;
|
||||||
|
private readonly Http2OutputFlowControl _outputFlowControl = new Http2OutputFlowControl(Http2PeerSettings.DefaultInitialWindowSize);
|
||||||
|
|
||||||
private readonly Http2PeerSettings _serverSettings = new Http2PeerSettings();
|
private readonly Http2PeerSettings _serverSettings = new Http2PeerSettings();
|
||||||
private readonly Http2PeerSettings _clientSettings = new Http2PeerSettings();
|
private readonly Http2PeerSettings _clientSettings = new Http2PeerSettings();
|
||||||
|
|
@ -80,7 +81,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
public Http2Connection(Http2ConnectionContext context)
|
public Http2Connection(Http2ConnectionContext context)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_frameWriter = new Http2FrameWriter(context.Transport.Output, context.Application.Input);
|
_frameWriter = new Http2FrameWriter(context.Transport.Output, context.Application.Input, _outputFlowControl, this);
|
||||||
_hpackDecoder = new HPackDecoder((int)_serverSettings.HeaderTableSize);
|
_hpackDecoder = new HPackDecoder((int)_serverSettings.HeaderTableSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,7 +96,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
public void OnInputOrOutputCompleted()
|
public void OnInputOrOutputCompleted()
|
||||||
{
|
{
|
||||||
_stopping = true;
|
_stopping = true;
|
||||||
_frameWriter.Abort(ex: null);
|
_frameWriter.Complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Abort(ConnectionAbortedException ex)
|
public void Abort(ConnectionAbortedException ex)
|
||||||
|
|
@ -143,7 +144,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
if (Http2FrameReader.ReadFrame(readableBuffer, _incomingFrame, _serverSettings.MaxFrameSize, out consumed, out examined))
|
if (Http2FrameReader.ReadFrame(readableBuffer, _incomingFrame, _serverSettings.MaxFrameSize, out consumed, out examined))
|
||||||
{
|
{
|
||||||
Log.LogTrace($"Connection id {ConnectionId} received {_incomingFrame.Type} frame with flags 0x{_incomingFrame.Flags:x} and length {_incomingFrame.Length} for stream ID {_incomingFrame.StreamId}");
|
Log.LogTrace($"Connection id {ConnectionId} received {_incomingFrame.Type} frame with flags 0x{_incomingFrame.Flags:x} and length {_incomingFrame.Length} for stream ID {_incomingFrame.StreamId}");
|
||||||
await ProcessFrameAsync<TContext>(application);
|
await ProcessFrameAsync(application);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (result.IsCompleted)
|
else if (result.IsCompleted)
|
||||||
|
|
@ -151,6 +152,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Http2StreamErrorException ex)
|
||||||
|
{
|
||||||
|
Log.Http2StreamError(ConnectionId, ex);
|
||||||
|
AbortStream(_incomingFrame.StreamId, new ConnectionAbortedException(ex.Message, ex));
|
||||||
|
await _frameWriter.WriteRstStreamAsync(ex.StreamId, ex.ErrorCode);
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
Input.AdvanceTo(consumed, examined);
|
Input.AdvanceTo(consumed, examined);
|
||||||
|
|
@ -187,20 +194,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
var connectionError = error as ConnectionAbortedException
|
||||||
|
?? new ConnectionAbortedException(CoreStrings.Http2ConnectionFaulted, error);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var stream in _streams.Values)
|
foreach (var stream in _streams.Values)
|
||||||
{
|
{
|
||||||
stream.Http2Abort(error);
|
stream.Abort(connectionError);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _frameWriter.WriteGoAwayAsync(_highestOpenedStreamId, errorCode);
|
await _frameWriter.WriteGoAwayAsync(_highestOpenedStreamId, errorCode);
|
||||||
_frameWriter.Complete();
|
_frameWriter.Complete();
|
||||||
}
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_frameWriter.Abort(connectionError);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
Input.Complete();
|
Input.Complete();
|
||||||
_frameWriter.Abort(ex: null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -296,7 +310,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
case Http2FrameType.DATA:
|
case Http2FrameType.DATA:
|
||||||
return ProcessDataFrameAsync();
|
return ProcessDataFrameAsync();
|
||||||
case Http2FrameType.HEADERS:
|
case Http2FrameType.HEADERS:
|
||||||
return ProcessHeadersFrameAsync<TContext>(application);
|
return ProcessHeadersFrameAsync(application);
|
||||||
case Http2FrameType.PRIORITY:
|
case Http2FrameType.PRIORITY:
|
||||||
return ProcessPriorityFrameAsync();
|
return ProcessPriorityFrameAsync();
|
||||||
case Http2FrameType.RST_STREAM:
|
case Http2FrameType.RST_STREAM:
|
||||||
|
|
@ -312,7 +326,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
case Http2FrameType.WINDOW_UPDATE:
|
case Http2FrameType.WINDOW_UPDATE:
|
||||||
return ProcessWindowUpdateFrameAsync();
|
return ProcessWindowUpdateFrameAsync();
|
||||||
case Http2FrameType.CONTINUATION:
|
case Http2FrameType.CONTINUATION:
|
||||||
return ProcessContinuationFrameAsync<TContext>(application);
|
return ProcessContinuationFrameAsync(application);
|
||||||
default:
|
default:
|
||||||
return ProcessUnknownFrameAsync();
|
return ProcessUnknownFrameAsync();
|
||||||
}
|
}
|
||||||
|
|
@ -442,7 +456,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
LocalEndPoint = _context.LocalEndPoint,
|
LocalEndPoint = _context.LocalEndPoint,
|
||||||
RemoteEndPoint = _context.RemoteEndPoint,
|
RemoteEndPoint = _context.RemoteEndPoint,
|
||||||
StreamLifetimeHandler = this,
|
StreamLifetimeHandler = this,
|
||||||
FrameWriter = _frameWriter
|
ClientPeerSettings = _clientSettings,
|
||||||
|
FrameWriter = _frameWriter,
|
||||||
|
ConnectionOutputFlowControl = _outputFlowControl,
|
||||||
|
TimeoutControl = this,
|
||||||
});
|
});
|
||||||
|
|
||||||
if ((_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_STREAM) == Http2HeadersFrameFlags.END_STREAM)
|
if ((_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_STREAM) == Http2HeadersFrameFlags.END_STREAM)
|
||||||
|
|
@ -500,11 +517,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
|
|
||||||
ThrowIfIncomingFrameSentToIdleStream();
|
ThrowIfIncomingFrameSentToIdleStream();
|
||||||
|
AbortStream(_incomingFrame.StreamId, new ConnectionAbortedException(CoreStrings.Http2StreamResetByClient));
|
||||||
if (_streams.TryGetValue(_incomingFrame.StreamId, out var stream))
|
|
||||||
{
|
|
||||||
stream.Abort(abortReason: null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
@ -533,7 +546,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// ParseFrame will not parse an InitialWindowSize > int.MaxValue.
|
||||||
|
var previousInitialWindowSize = (int)_clientSettings.InitialWindowSize;
|
||||||
|
|
||||||
_clientSettings.ParseFrame(_incomingFrame);
|
_clientSettings.ParseFrame(_incomingFrame);
|
||||||
|
|
||||||
|
// This difference can be negative.
|
||||||
|
var windowSizeDifference = (int)_clientSettings.InitialWindowSize - previousInitialWindowSize;
|
||||||
|
|
||||||
|
if (windowSizeDifference != 0)
|
||||||
|
{
|
||||||
|
foreach (var stream in _streams.Values)
|
||||||
|
{
|
||||||
|
if (!stream.TryUpdateOutputWindow(windowSizeDifference))
|
||||||
|
{
|
||||||
|
// This means that this caused a stream window to become larger than int.MaxValue.
|
||||||
|
// This can never happen with a well behaved client and MUST be treated as a connection error.
|
||||||
|
// https://httpwg.org/specs/rfc7540.html#rfc.section.6.9.2
|
||||||
|
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorInitialWindowSizeInvalid, Http2ErrorCode.FLOW_CONTROL_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return _frameWriter.WriteSettingsAckAsync();
|
return _frameWriter.WriteSettingsAckAsync();
|
||||||
}
|
}
|
||||||
catch (Http2SettingsParameterOutOfRangeException ex)
|
catch (Http2SettingsParameterOutOfRangeException ex)
|
||||||
|
|
@ -620,6 +654,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorWindowUpdateIncrementZero, Http2ErrorCode.PROTOCOL_ERROR);
|
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorWindowUpdateIncrementZero, Http2ErrorCode.PROTOCOL_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_incomingFrame.StreamId == 0)
|
||||||
|
{
|
||||||
|
if (!_frameWriter.TryUpdateConnectionWindow(_incomingFrame.WindowUpdateSizeIncrement))
|
||||||
|
{
|
||||||
|
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorWindowUpdateSizeInvalid, Http2ErrorCode.FLOW_CONTROL_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (_streams.TryGetValue(_incomingFrame.StreamId, out var stream))
|
||||||
|
{
|
||||||
|
if (!stream.TryUpdateOutputWindow(_incomingFrame.WindowUpdateSizeIncrement))
|
||||||
|
{
|
||||||
|
throw new Http2StreamErrorException(_incomingFrame.StreamId, CoreStrings.Http2ErrorWindowUpdateSizeInvalid, Http2ErrorCode.FLOW_CONTROL_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// The stream was not found in the dictionary which means the stream was probably closed. This can
|
||||||
|
// happen when the client sends a window update for a stream right as the server closes the same stream
|
||||||
|
// Since this is an unavoidable race, we just ignore the window update frame.
|
||||||
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -669,11 +724,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
ResetRequestHeaderParsingState();
|
ResetRequestHeaderParsingState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Http2StreamErrorException ex)
|
catch (Http2StreamErrorException)
|
||||||
{
|
{
|
||||||
Log.Http2StreamError(ConnectionId, ex);
|
|
||||||
ResetRequestHeaderParsingState();
|
ResetRequestHeaderParsingState();
|
||||||
return _frameWriter.WriteRstStreamAsync(ex.StreamId, ex.ErrorCode);
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
@ -745,6 +799,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AbortStream(int streamId, ConnectionAbortedException error)
|
||||||
|
{
|
||||||
|
if (_streams.TryGetValue(streamId, out var stream))
|
||||||
|
{
|
||||||
|
stream.Abort(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void IHttp2StreamLifetimeHandler.OnStreamCompleted(int streamId)
|
void IHttp2StreamLifetimeHandler.OnStreamCompleted(int streamId)
|
||||||
{
|
{
|
||||||
_streams.TryRemove(streamId, out _);
|
_streams.TryRemove(streamId, out _);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
{
|
{
|
||||||
public int WindowUpdateSizeIncrement
|
public int WindowUpdateSizeIncrement
|
||||||
{
|
{
|
||||||
get => ((Payload[0] << 24) | (Payload[1] << 16) | (Payload[2] << 16) | Payload[3]) & 0x7fffffff;
|
get => ((Payload[0] << 24) | (Payload[1] << 16) | (Payload[2] << 8) | Payload[3]) & 0x7fffffff;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
Payload[0] = (byte)(((uint)value >> 24) & 0x7f);
|
Payload[0] = (byte)(((uint)value >> 24) & 0x7f);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
{
|
{
|
||||||
public partial class Http2Frame
|
public partial class Http2Frame
|
||||||
{
|
{
|
||||||
public const int MinAllowedMaxFrameSize = 16 * 1024;
|
public const int MinAllowedMaxFrameSize = 16 * 1024;
|
||||||
public const int MaxAllowedMaxFrameSize = 16 * 1024 * 1024 - 1;
|
public const int MaxAllowedMaxFrameSize = 16 * 1024 * 1024 - 1;
|
||||||
public const int HeaderLength = 9;
|
public const int HeaderLength = 9;
|
||||||
|
|
||||||
|
|
@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
get => _data[FlagsOffset];
|
get => _data[FlagsOffset];
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_data[FlagsOffset] = (byte)value;
|
_data[FlagsOffset] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,15 @@ using System.Collections.Generic;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Connections;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
{
|
{
|
||||||
public class Http2FrameWriter : IHttp2FrameWriter
|
public class Http2FrameWriter
|
||||||
{
|
{
|
||||||
// Literal Header Field without Indexing - Indexed Name (Index 8 - :status)
|
// Literal Header Field without Indexing - Indexed Name (Index 8 - :status)
|
||||||
private static readonly byte[] _continueBytes = new byte[] { 0x08, 0x03, (byte)'1', (byte)'0', (byte)'0' };
|
private static readonly byte[] _continueBytes = new byte[] { 0x08, 0x03, (byte)'1', (byte)'0', (byte)'0' };
|
||||||
|
|
@ -23,13 +25,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
private readonly HPackEncoder _hpackEncoder = new HPackEncoder();
|
private readonly HPackEncoder _hpackEncoder = new HPackEncoder();
|
||||||
private readonly PipeWriter _outputWriter;
|
private readonly PipeWriter _outputWriter;
|
||||||
private readonly PipeReader _outputReader;
|
private readonly PipeReader _outputReader;
|
||||||
|
private readonly Http2OutputFlowControl _connectionOutputFlowControl;
|
||||||
|
private readonly StreamSafePipeFlusher _flusher;
|
||||||
|
|
||||||
private bool _completed;
|
private bool _completed;
|
||||||
|
|
||||||
public Http2FrameWriter(PipeWriter outputPipeWriter, PipeReader outputPipeReader)
|
public Http2FrameWriter(
|
||||||
|
PipeWriter outputPipeWriter,
|
||||||
|
PipeReader outputPipeReader,
|
||||||
|
Http2OutputFlowControl connectionOutputFlowControl,
|
||||||
|
ITimeoutControl timeoutControl)
|
||||||
{
|
{
|
||||||
_outputWriter = outputPipeWriter;
|
_outputWriter = outputPipeWriter;
|
||||||
_outputReader = outputPipeReader;
|
_outputReader = outputPipeReader;
|
||||||
|
|
||||||
|
_connectionOutputFlowControl = connectionOutputFlowControl;
|
||||||
|
_flusher = new StreamSafePipeFlusher(_outputWriter, timeoutControl);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Complete()
|
public void Complete()
|
||||||
|
|
@ -42,30 +53,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
|
|
||||||
_completed = true;
|
_completed = true;
|
||||||
|
_connectionOutputFlowControl.Abort();
|
||||||
_outputWriter.Complete();
|
_outputWriter.Complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Abort(Exception ex)
|
public void Abort(ConnectionAbortedException ex)
|
||||||
|
{
|
||||||
|
// TODO: Really abort the connection using the ConnectionContex like Http1OutputProducer.
|
||||||
|
_outputReader.CancelPendingRead();
|
||||||
|
Complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task FlushAsync(IHttpOutputProducer outputProducer, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
lock (_writeLock)
|
lock (_writeLock)
|
||||||
{
|
{
|
||||||
if (_completed)
|
if (_completed)
|
||||||
{
|
{
|
||||||
return;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
_completed = true;
|
return _flusher.FlushAsync(0, outputProducer, cancellationToken);
|
||||||
_outputReader.CancelPendingRead();
|
|
||||||
_outputWriter.Complete(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task FlushAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
lock (_writeLock)
|
|
||||||
{
|
|
||||||
return WriteAsync(Constants.EmptyData);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,7 +86,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
_outgoingFrame.Length = _continueBytes.Length;
|
_outgoingFrame.Length = _continueBytes.Length;
|
||||||
_continueBytes.CopyTo(_outgoingFrame.HeadersPayload);
|
_continueBytes.CopyTo(_outgoingFrame.HeadersPayload);
|
||||||
|
|
||||||
return WriteAsync(_outgoingFrame.Raw);
|
return WriteUnsynchronizedAsync(_outgoingFrame.Raw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,6 +94,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
{
|
{
|
||||||
lock (_writeLock)
|
lock (_writeLock)
|
||||||
{
|
{
|
||||||
|
if (_completed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_outgoingFrame.PrepareHeaders(Http2HeadersFrameFlags.NONE, streamId);
|
_outgoingFrame.PrepareHeaders(Http2HeadersFrameFlags.NONE, streamId);
|
||||||
|
|
||||||
var done = _hpackEncoder.BeginEncode(statusCode, EnumerateHeaders(headers), _outgoingFrame.Payload, out var payloadLength);
|
var done = _hpackEncoder.BeginEncode(statusCode, EnumerateHeaders(headers), _outgoingFrame.Payload, out var payloadLength);
|
||||||
|
|
@ -95,7 +109,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
_outgoingFrame.HeadersFlags = Http2HeadersFrameFlags.END_HEADERS;
|
_outgoingFrame.HeadersFlags = Http2HeadersFrameFlags.END_HEADERS;
|
||||||
}
|
}
|
||||||
|
|
||||||
Append(_outgoingFrame.Raw);
|
_outputWriter.Write(_outgoingFrame.Raw);
|
||||||
|
|
||||||
while (!done)
|
while (!done)
|
||||||
{
|
{
|
||||||
|
|
@ -109,51 +123,128 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
_outgoingFrame.ContinuationFlags = Http2ContinuationFrameFlags.END_HEADERS;
|
_outgoingFrame.ContinuationFlags = Http2ContinuationFrameFlags.END_HEADERS;
|
||||||
}
|
}
|
||||||
|
|
||||||
Append(_outgoingFrame.Raw);
|
_outputWriter.Write(_outgoingFrame.Raw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteDataAsync(int streamId, ReadOnlySpan<byte> data, CancellationToken cancellationToken)
|
public Task WriteDataAsync(int streamId, Http2StreamOutputFlowControl flowControl, ReadOnlySequence<byte> data, bool endStream)
|
||||||
=> WriteDataAsync(streamId, data, endStream: false, cancellationToken: cancellationToken);
|
|
||||||
|
|
||||||
public Task WriteDataAsync(int streamId, ReadOnlySpan<byte> data, bool endStream, CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
var tasks = new List<Task>();
|
// The Length property of a ReadOnlySequence can be expensive, so we cache the value.
|
||||||
|
var dataLength = data.Length;
|
||||||
|
|
||||||
lock (_writeLock)
|
lock (_writeLock)
|
||||||
{
|
{
|
||||||
_outgoingFrame.PrepareData(streamId);
|
if (_completed || flowControl.IsAborted)
|
||||||
|
|
||||||
while (data.Length > _outgoingFrame.Length)
|
|
||||||
{
|
{
|
||||||
data.Slice(0, _outgoingFrame.Length).CopyTo(_outgoingFrame.Payload);
|
return Task.CompletedTask;
|
||||||
data = data.Slice(_outgoingFrame.Length);
|
|
||||||
|
|
||||||
tasks.Add(WriteAsync(_outgoingFrame.Raw, cancellationToken));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_outgoingFrame.Length = data.Length;
|
// Zero-length data frames are allowed to be sent immediately even if there is no space available in the flow control window.
|
||||||
|
// https://httpwg.org/specs/rfc7540.html#rfc.section.6.9.1
|
||||||
if (endStream)
|
if (dataLength != 0 && dataLength > flowControl.Available)
|
||||||
{
|
{
|
||||||
_outgoingFrame.DataFlags = Http2DataFrameFlags.END_STREAM;
|
return WriteDataAsyncAwaited(streamId, flowControl, data, dataLength, endStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
data.CopyTo(_outgoingFrame.Payload);
|
// This cast is safe since if dataLength would overflow an int, it's guaranteed to be greater than the available flow control window.
|
||||||
|
flowControl.Advance((int)dataLength);
|
||||||
tasks.Add(WriteAsync(_outgoingFrame.Raw, cancellationToken));
|
return WriteDataUnsynchronizedAsync(streamId, data, endStream);
|
||||||
|
|
||||||
return Task.WhenAll(tasks);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Task WriteDataUnsynchronizedAsync(int streamId, ReadOnlySequence<byte> data, bool endStream)
|
||||||
|
{
|
||||||
|
_outgoingFrame.PrepareData(streamId);
|
||||||
|
|
||||||
|
var payload = _outgoingFrame.Payload;
|
||||||
|
var unwrittenPayloadLength = 0;
|
||||||
|
|
||||||
|
foreach (var buffer in data)
|
||||||
|
{
|
||||||
|
var current = buffer;
|
||||||
|
|
||||||
|
while (current.Length > payload.Length)
|
||||||
|
{
|
||||||
|
current.Span.Slice(0, payload.Length).CopyTo(payload);
|
||||||
|
current = current.Slice(payload.Length);
|
||||||
|
|
||||||
|
_outputWriter.Write(_outgoingFrame.Raw);
|
||||||
|
payload = _outgoingFrame.Payload;
|
||||||
|
unwrittenPayloadLength = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.Length > 0)
|
||||||
|
{
|
||||||
|
current.Span.CopyTo(payload);
|
||||||
|
payload = payload.Slice(current.Length);
|
||||||
|
unwrittenPayloadLength += current.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endStream)
|
||||||
|
{
|
||||||
|
_outgoingFrame.DataFlags = Http2DataFrameFlags.END_STREAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
_outgoingFrame.Length = unwrittenPayloadLength;
|
||||||
|
_outputWriter.Write(_outgoingFrame.Raw);
|
||||||
|
|
||||||
|
return FlushUnsynchronizedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WriteDataAsyncAwaited(int streamId, Http2StreamOutputFlowControl flowControl, ReadOnlySequence<byte> data, long dataLength, bool endStream)
|
||||||
|
{
|
||||||
|
while (dataLength > 0)
|
||||||
|
{
|
||||||
|
Http2OutputFlowControlAwaitable availabilityAwaitable;
|
||||||
|
var writeTask = Task.CompletedTask;
|
||||||
|
|
||||||
|
lock (_writeLock)
|
||||||
|
{
|
||||||
|
if (_completed || flowControl.IsAborted)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var actual = flowControl.AdvanceUpToAndWait(dataLength, out availabilityAwaitable);
|
||||||
|
|
||||||
|
if (actual > 0)
|
||||||
|
{
|
||||||
|
if (actual < dataLength)
|
||||||
|
{
|
||||||
|
writeTask = WriteDataUnsynchronizedAsync(streamId, data.Slice(0, actual), endStream: false);
|
||||||
|
data = data.Slice(actual);
|
||||||
|
dataLength -= actual;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
writeTask = WriteDataUnsynchronizedAsync(streamId, data, endStream);
|
||||||
|
dataLength = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This awaitable releases continuations in FIFO order when the window updates.
|
||||||
|
// It should be very rare for a continuation to run without any availability.
|
||||||
|
if (availabilityAwaitable != null)
|
||||||
|
{
|
||||||
|
await availabilityAwaitable;
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that the application continuation isn't executed inline by ProcessWindowUpdateFrameAsync.
|
||||||
|
await ThreadPoolAwaitable.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
public Task WriteRstStreamAsync(int streamId, Http2ErrorCode errorCode)
|
public Task WriteRstStreamAsync(int streamId, Http2ErrorCode errorCode)
|
||||||
{
|
{
|
||||||
lock (_writeLock)
|
lock (_writeLock)
|
||||||
{
|
{
|
||||||
_outgoingFrame.PrepareRstStream(streamId, errorCode);
|
_outgoingFrame.PrepareRstStream(streamId, errorCode);
|
||||||
return WriteAsync(_outgoingFrame.Raw);
|
return WriteUnsynchronizedAsync(_outgoingFrame.Raw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,7 +254,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
{
|
{
|
||||||
// TODO: actually send settings
|
// TODO: actually send settings
|
||||||
_outgoingFrame.PrepareSettings(Http2SettingsFrameFlags.NONE);
|
_outgoingFrame.PrepareSettings(Http2SettingsFrameFlags.NONE);
|
||||||
return WriteAsync(_outgoingFrame.Raw);
|
return WriteUnsynchronizedAsync(_outgoingFrame.Raw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,7 +263,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
lock (_writeLock)
|
lock (_writeLock)
|
||||||
{
|
{
|
||||||
_outgoingFrame.PrepareSettings(Http2SettingsFrameFlags.ACK);
|
_outgoingFrame.PrepareSettings(Http2SettingsFrameFlags.ACK);
|
||||||
return WriteAsync(_outgoingFrame.Raw);
|
return WriteUnsynchronizedAsync(_outgoingFrame.Raw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,7 +273,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
{
|
{
|
||||||
_outgoingFrame.PreparePing(Http2PingFrameFlags.ACK);
|
_outgoingFrame.PreparePing(Http2PingFrameFlags.ACK);
|
||||||
payload.CopyTo(_outgoingFrame.Payload);
|
payload.CopyTo(_outgoingFrame.Payload);
|
||||||
return WriteAsync(_outgoingFrame.Raw);
|
return WriteUnsynchronizedAsync(_outgoingFrame.Raw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,23 +282,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
lock (_writeLock)
|
lock (_writeLock)
|
||||||
{
|
{
|
||||||
_outgoingFrame.PrepareGoAway(lastStreamId, errorCode);
|
_outgoingFrame.PrepareGoAway(lastStreamId, errorCode);
|
||||||
return WriteAsync(_outgoingFrame.Raw);
|
return WriteUnsynchronizedAsync(_outgoingFrame.Raw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must be called with _writeLock
|
private Task WriteUnsynchronizedAsync(ReadOnlySpan<byte> data)
|
||||||
private void Append(ReadOnlySpan<byte> data)
|
|
||||||
{
|
|
||||||
if (_completed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_outputWriter.Write(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Must be called with _writeLock
|
|
||||||
private Task WriteAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken = default(CancellationToken))
|
|
||||||
{
|
{
|
||||||
if (_completed)
|
if (_completed)
|
||||||
{
|
{
|
||||||
|
|
@ -215,12 +294,36 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
|
|
||||||
_outputWriter.Write(data);
|
_outputWriter.Write(data);
|
||||||
return FlushAsync(_outputWriter, cancellationToken);
|
return FlushUnsynchronizedAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task FlushAsync(PipeWriter outputWriter, CancellationToken cancellationToken)
|
private Task FlushUnsynchronizedAsync()
|
||||||
{
|
{
|
||||||
await outputWriter.FlushAsync(cancellationToken);
|
return _flusher.FlushAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryUpdateConnectionWindow(int bytes)
|
||||||
|
{
|
||||||
|
lock (_writeLock)
|
||||||
|
{
|
||||||
|
return _connectionOutputFlowControl.TryUpdateWindow(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryUpdateStreamWindow(Http2StreamOutputFlowControl flowControl, int bytes)
|
||||||
|
{
|
||||||
|
lock (_writeLock)
|
||||||
|
{
|
||||||
|
return flowControl.TryUpdateWindow(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AbortPendingStreamDataWrites(Http2StreamOutputFlowControl flowControl)
|
||||||
|
{
|
||||||
|
lock (_writeLock)
|
||||||
|
{
|
||||||
|
flowControl.Abort();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<KeyValuePair<string, string>> EnumerateHeaders(IHeaderDictionary headers)
|
private static IEnumerable<KeyValuePair<string, string>> EnumerateHeaders(IHeaderDictionary headers)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
// 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.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
|
{
|
||||||
|
public class Http2OutputFlowControl
|
||||||
|
{
|
||||||
|
private readonly Queue<Http2OutputFlowControlAwaitable> _awaitableQueue = new Queue<Http2OutputFlowControlAwaitable>();
|
||||||
|
|
||||||
|
public Http2OutputFlowControl(uint initialWindowSize)
|
||||||
|
{
|
||||||
|
Debug.Assert(initialWindowSize <= Http2PeerSettings.MaxWindowSize, $"{nameof(initialWindowSize)} too large.");
|
||||||
|
|
||||||
|
Available = (int)initialWindowSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Available { get; private set; }
|
||||||
|
public bool IsAborted { get; private set; }
|
||||||
|
|
||||||
|
public Http2OutputFlowControlAwaitable AvailabilityAwaitable
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
Debug.Assert(!IsAborted, $"({nameof(AvailabilityAwaitable)} accessed after abort.");
|
||||||
|
Debug.Assert(Available <= 0, $"({nameof(AvailabilityAwaitable)} accessed with {Available} bytes available.");
|
||||||
|
|
||||||
|
var awaitable = new Http2OutputFlowControlAwaitable();
|
||||||
|
_awaitableQueue.Enqueue(awaitable);
|
||||||
|
return awaitable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Advance(int bytes)
|
||||||
|
{
|
||||||
|
Debug.Assert(!IsAborted, $"({nameof(Advance)} called after abort.");
|
||||||
|
Debug.Assert(bytes == 0 || (bytes > 0 && bytes <= Available), $"{nameof(Advance)}({bytes}) called with {Available} bytes available.");
|
||||||
|
|
||||||
|
Available -= bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bytes can be negative when SETTINGS_INITIAL_WINDOW_SIZE decreases mid-connection.
|
||||||
|
// This can also cause Available to become negative which MUST be allowed.
|
||||||
|
// https://httpwg.org/specs/rfc7540.html#rfc.section.6.9.2
|
||||||
|
public bool TryUpdateWindow(int bytes)
|
||||||
|
{
|
||||||
|
var maxUpdate = Http2PeerSettings.MaxWindowSize - Available;
|
||||||
|
|
||||||
|
if (bytes > maxUpdate)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Available += bytes;
|
||||||
|
|
||||||
|
while (Available > 0 && _awaitableQueue.Count > 0)
|
||||||
|
{
|
||||||
|
var awaitable = _awaitableQueue.Dequeue();
|
||||||
|
awaitable.Complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Abort()
|
||||||
|
{
|
||||||
|
IsAborted = true;
|
||||||
|
|
||||||
|
while (_awaitableQueue.Count > 0)
|
||||||
|
{
|
||||||
|
_awaitableQueue.Dequeue().Complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
// 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.Diagnostics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
|
{
|
||||||
|
public class Http2OutputFlowControlAwaitable : ICriticalNotifyCompletion
|
||||||
|
{
|
||||||
|
private static readonly Action _callbackCompleted = () => { };
|
||||||
|
|
||||||
|
private Action _callback;
|
||||||
|
|
||||||
|
public Http2OutputFlowControlAwaitable GetAwaiter() => this;
|
||||||
|
public bool IsCompleted => ReferenceEquals(_callback, _callbackCompleted);
|
||||||
|
|
||||||
|
public void GetResult()
|
||||||
|
{
|
||||||
|
Debug.Assert(ReferenceEquals(_callback, _callbackCompleted));
|
||||||
|
|
||||||
|
_callback = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnCompleted(Action continuation)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(_callback, _callbackCompleted) ||
|
||||||
|
ReferenceEquals(Interlocked.CompareExchange(ref _callback, continuation, null), _callbackCompleted))
|
||||||
|
{
|
||||||
|
continuation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnsafeOnCompleted(Action continuation)
|
||||||
|
{
|
||||||
|
OnCompleted(continuation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Complete()
|
||||||
|
{
|
||||||
|
var continuation = Interlocked.Exchange(ref _callback, _callbackCompleted);
|
||||||
|
|
||||||
|
continuation?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,33 +2,79 @@
|
||||||
// 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.Diagnostics;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Connections;
|
using Microsoft.AspNetCore.Connections;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||||
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.Http2
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
{
|
{
|
||||||
public class Http2OutputProducer : IHttpOutputProducer
|
public class Http2OutputProducer : IHttpOutputProducer
|
||||||
{
|
{
|
||||||
private readonly int _streamId;
|
private readonly int _streamId;
|
||||||
private readonly IHttp2FrameWriter _frameWriter;
|
private readonly Http2FrameWriter _frameWriter;
|
||||||
|
private readonly StreamSafePipeFlusher _flusher;
|
||||||
|
|
||||||
public Http2OutputProducer(int streamId, IHttp2FrameWriter frameWriter)
|
// This should only be accessed via the FrameWriter. The connection-level output flow control is protected by the
|
||||||
|
// FrameWriter's connection-level write lock.
|
||||||
|
private readonly Http2StreamOutputFlowControl _flowControl;
|
||||||
|
|
||||||
|
private readonly object _dataWriterLock = new object();
|
||||||
|
private readonly Pipe _dataPipe;
|
||||||
|
private readonly Task _dataWriteProcessingTask;
|
||||||
|
private bool _startedWritingDataFrames;
|
||||||
|
private bool _completed;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public Http2OutputProducer(
|
||||||
|
int streamId,
|
||||||
|
Http2FrameWriter frameWriter,
|
||||||
|
Http2StreamOutputFlowControl flowControl,
|
||||||
|
ITimeoutControl timeoutControl,
|
||||||
|
MemoryPool<byte> pool)
|
||||||
{
|
{
|
||||||
_streamId = streamId;
|
_streamId = streamId;
|
||||||
_frameWriter = frameWriter;
|
_frameWriter = frameWriter;
|
||||||
|
_flowControl = flowControl;
|
||||||
|
_dataPipe = CreateDataPipe(pool);
|
||||||
|
_flusher = new StreamSafePipeFlusher(_dataPipe.Writer, timeoutControl);
|
||||||
|
_dataWriteProcessingTask = ProcessDataWrites();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
lock (_dataWriterLock)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
|
||||||
|
if (!_completed)
|
||||||
|
{
|
||||||
|
_completed = true;
|
||||||
|
|
||||||
|
// Complete with an exception to prevent an end of stream data frame from being sent without an
|
||||||
|
// explicit call to WriteStreamSuffixAsync. ConnectionAbortedExceptions are swallowed, so the
|
||||||
|
// message doesn't matter
|
||||||
|
_dataPipe.Writer.Complete(new ConnectionAbortedException());
|
||||||
|
}
|
||||||
|
|
||||||
|
_frameWriter.AbortPendingStreamDataWrites(_flowControl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Abort(ConnectionAbortedException error)
|
public void Abort(ConnectionAbortedException abortReason)
|
||||||
{
|
{
|
||||||
// TODO: RST_STREAM?
|
// TODO: RST_STREAM?
|
||||||
|
Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteAsync<T>(Func<PipeWriter, T, long> callback, T state)
|
public Task WriteAsync<T>(Func<PipeWriter, T, long> callback, T state)
|
||||||
|
|
@ -36,23 +82,142 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task FlushAsync(CancellationToken cancellationToken) => _frameWriter.FlushAsync(cancellationToken);
|
public Task FlushAsync(CancellationToken cancellationToken)
|
||||||
|
|
||||||
public Task Write100ContinueAsync(CancellationToken cancellationToken) => _frameWriter.Write100ContinueAsync(_streamId);
|
|
||||||
|
|
||||||
public Task WriteDataAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
return _frameWriter.WriteDataAsync(_streamId, data, cancellationToken);
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return Task.FromCanceled(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_dataWriterLock)
|
||||||
|
{
|
||||||
|
if (_completed)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_startedWritingDataFrames)
|
||||||
|
{
|
||||||
|
// If there's already been response data written to the stream, just wait for that. Any header
|
||||||
|
// should be in front of the data frames in the connection pipe. Trailers could change things.
|
||||||
|
return _flusher.FlushAsync(0, this, cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Flushing the connection pipe ensures headers already in the pipe are flushed even if no data
|
||||||
|
// frames have been written.
|
||||||
|
return _frameWriter.FlushAsync(this, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteStreamSuffixAsync(CancellationToken cancellationToken)
|
public Task Write100ContinueAsync()
|
||||||
{
|
{
|
||||||
return _frameWriter.WriteDataAsync(_streamId, Constants.EmptyData, endStream: true, cancellationToken: cancellationToken);
|
lock (_dataWriterLock)
|
||||||
|
{
|
||||||
|
if (_completed)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _frameWriter.Write100ContinueAsync(_streamId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void WriteResponseHeaders(int statusCode, string ReasonPhrase, HttpResponseHeaders responseHeaders)
|
public void WriteResponseHeaders(int statusCode, string ReasonPhrase, HttpResponseHeaders responseHeaders)
|
||||||
{
|
{
|
||||||
_frameWriter.WriteResponseHeaders(_streamId, statusCode, responseHeaders);
|
lock (_dataWriterLock)
|
||||||
|
{
|
||||||
|
if (_completed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The HPACK header compressor is stateful, if we compress headers for an aborted stream we must send them.
|
||||||
|
// Optimize for not compressing or sending them.
|
||||||
|
_frameWriter.WriteResponseHeaders(_streamId, statusCode, responseHeaders);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task WriteDataAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return Task.FromCanceled(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 Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
_startedWritingDataFrames = true;
|
||||||
|
|
||||||
|
_dataPipe.Writer.Write(data);
|
||||||
|
return _flusher.FlushAsync(data.Length, this, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task WriteStreamSuffixAsync()
|
||||||
|
{
|
||||||
|
lock (_dataWriterLock)
|
||||||
|
{
|
||||||
|
if (_completed)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
_completed = true;
|
||||||
|
|
||||||
|
// Even if there's no actual data, completing the writer gracefully sends an END_STREAM DATA frame.
|
||||||
|
_startedWritingDataFrames = true;
|
||||||
|
|
||||||
|
_dataPipe.Writer.Complete();
|
||||||
|
return _dataWriteProcessingTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessDataWrites()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ReadResult readResult;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
readResult = await _dataPipe.Reader.ReadAsync();
|
||||||
|
|
||||||
|
await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream: readResult.IsCompleted);
|
||||||
|
|
||||||
|
_dataPipe.Reader.AdvanceTo(readResult.Buffer.End);
|
||||||
|
} while (!readResult.IsCompleted);
|
||||||
|
}
|
||||||
|
catch (ConnectionAbortedException)
|
||||||
|
{
|
||||||
|
// Writes should not throw for aborted connections.
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.Assert(false, ex.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
_dataPipe.Reader.Complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Pipe CreateDataPipe(MemoryPool<byte> pool)
|
||||||
|
=> new Pipe(new PipeOptions
|
||||||
|
(
|
||||||
|
pool: pool,
|
||||||
|
readerScheduler: PipeScheduler.Inline,
|
||||||
|
writerScheduler: PipeScheduler.Inline,
|
||||||
|
pauseWriterThreshold: 1,
|
||||||
|
resumeWriterThreshold: 1,
|
||||||
|
useSynchronizationContext: false,
|
||||||
|
minimumSegmentSize: KestrelMemoryPool.MinimumSegmentSize
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
public const uint DefaultInitialWindowSize = 65535;
|
public const uint DefaultInitialWindowSize = 65535;
|
||||||
public const uint DefaultMaxFrameSize = 16384;
|
public const uint DefaultMaxFrameSize = 16384;
|
||||||
public const uint DefaultMaxHeaderListSize = uint.MaxValue;
|
public const uint DefaultMaxHeaderListSize = uint.MaxValue;
|
||||||
|
public const uint MaxWindowSize = int.MaxValue;
|
||||||
|
|
||||||
public uint HeaderTableSize { get; set; } = DefaultHeaderTableSize;
|
public uint HeaderTableSize { get; set; } = DefaultHeaderTableSize;
|
||||||
|
|
||||||
|
|
@ -59,11 +60,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
MaxConcurrentStreams = value;
|
MaxConcurrentStreams = value;
|
||||||
break;
|
break;
|
||||||
case Http2SettingsParameter.SETTINGS_INITIAL_WINDOW_SIZE:
|
case Http2SettingsParameter.SETTINGS_INITIAL_WINDOW_SIZE:
|
||||||
if (value > int.MaxValue)
|
if (value > MaxWindowSize)
|
||||||
{
|
{
|
||||||
throw new Http2SettingsParameterOutOfRangeException(Http2SettingsParameter.SETTINGS_INITIAL_WINDOW_SIZE,
|
throw new Http2SettingsParameterOutOfRangeException(Http2SettingsParameter.SETTINGS_INITIAL_WINDOW_SIZE,
|
||||||
lowerBound: 0,
|
lowerBound: 0,
|
||||||
upperBound: int.MaxValue);
|
upperBound: MaxWindowSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
InitialWindowSize = value;
|
InitialWindowSize = value;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
{
|
{
|
||||||
public class Http2SettingsParameterOutOfRangeException : Exception
|
public class Http2SettingsParameterOutOfRangeException : Exception
|
||||||
{
|
{
|
||||||
public Http2SettingsParameterOutOfRangeException(Http2SettingsParameter parameter, uint lowerBound, uint upperBound)
|
public Http2SettingsParameterOutOfRangeException(Http2SettingsParameter parameter, long lowerBound, long upperBound)
|
||||||
: base($"HTTP/2 SETTINGS parameter {parameter} must be set to a value between {lowerBound} and {upperBound}")
|
: base($"HTTP/2 SETTINGS parameter {parameter} must be set to a value between {lowerBound} and {upperBound}")
|
||||||
{
|
{
|
||||||
Parameter = parameter;
|
Parameter = parameter;
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,12 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
|
using System.IO;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Connections;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
|
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||||
using Microsoft.Extensions.Primitives;
|
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
|
|
@ -17,13 +16,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
public partial class Http2Stream : HttpProtocol
|
public partial class Http2Stream : HttpProtocol
|
||||||
{
|
{
|
||||||
private readonly Http2StreamContext _context;
|
private readonly Http2StreamContext _context;
|
||||||
|
private readonly Http2StreamOutputFlowControl _outputFlowControl;
|
||||||
|
|
||||||
public Http2Stream(Http2StreamContext context)
|
public Http2Stream(Http2StreamContext context)
|
||||||
: base(context)
|
: base(context)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_outputFlowControl = new Http2StreamOutputFlowControl(context.ConnectionOutputFlowControl, context.ClientPeerSettings.InitialWindowSize);
|
||||||
|
|
||||||
Output = new Http2OutputProducer(StreamId, _context.FrameWriter);
|
Output = new Http2OutputProducer(context.StreamId, context.FrameWriter, _outputFlowControl, context.TimeoutControl, context.MemoryPool);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int StreamId => _context.StreamId;
|
public int StreamId => _context.StreamId;
|
||||||
|
|
@ -143,15 +144,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: The HTTP/2 tests expect the request and response streams to be aborted with
|
public override void Abort(ConnectionAbortedException abortReason)
|
||||||
// non-ConnectionAbortedExceptions. The abortReasons can include things like
|
|
||||||
// Http2ConnectionErrorException which don't derive from IOException or
|
|
||||||
// OperationCanceledException. This is probably not a good idea.
|
|
||||||
public void Http2Abort(Exception abortReason)
|
|
||||||
{
|
{
|
||||||
_streams?.Abort(abortReason);
|
base.Abort(abortReason);
|
||||||
|
|
||||||
OnInputOrOutputCompleted();
|
// Unblock the request body.
|
||||||
|
RequestBodyPipe.Writer.Complete(new IOException(CoreStrings.Http2StreamAborted, abortReason));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryUpdateOutputWindow(int bytes)
|
||||||
|
{
|
||||||
|
return _context.FrameWriter.TryUpdateStreamWindow(_outputFlowControl, bytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System.Buffers;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
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.Internal.Infrastructure;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
{
|
{
|
||||||
|
|
@ -18,6 +19,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
public IPEndPoint RemoteEndPoint { get; set; }
|
public IPEndPoint RemoteEndPoint { get; set; }
|
||||||
public IPEndPoint LocalEndPoint { get; set; }
|
public IPEndPoint LocalEndPoint { get; set; }
|
||||||
public IHttp2StreamLifetimeHandler StreamLifetimeHandler { get; set; }
|
public IHttp2StreamLifetimeHandler StreamLifetimeHandler { get; set; }
|
||||||
public IHttp2FrameWriter FrameWriter { get; set; }
|
public Http2PeerSettings ClientPeerSettings { get; set; }
|
||||||
|
public Http2FrameWriter FrameWriter { get; set; }
|
||||||
|
public Http2OutputFlowControl ConnectionOutputFlowControl { get; set; }
|
||||||
|
public ITimeoutControl TimeoutControl { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
// 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.Diagnostics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
|
{
|
||||||
|
public class Http2StreamOutputFlowControl
|
||||||
|
{
|
||||||
|
private readonly Http2OutputFlowControl _connectionLevelFlowControl;
|
||||||
|
private readonly Http2OutputFlowControl _streamLevelFlowControl;
|
||||||
|
|
||||||
|
private Http2OutputFlowControlAwaitable _currentConnectionLevelAwaitable;
|
||||||
|
|
||||||
|
public Http2StreamOutputFlowControl(Http2OutputFlowControl connectionLevelFlowControl, uint initialWindowSize)
|
||||||
|
{
|
||||||
|
_connectionLevelFlowControl = connectionLevelFlowControl;
|
||||||
|
_streamLevelFlowControl = new Http2OutputFlowControl(initialWindowSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Available => Math.Min(_connectionLevelFlowControl.Available, _streamLevelFlowControl.Available);
|
||||||
|
|
||||||
|
public bool IsAborted => _connectionLevelFlowControl.IsAborted || _streamLevelFlowControl.IsAborted;
|
||||||
|
|
||||||
|
public void Advance(int bytes)
|
||||||
|
{
|
||||||
|
_connectionLevelFlowControl.Advance(bytes);
|
||||||
|
_streamLevelFlowControl.Advance(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int AdvanceUpToAndWait(long bytes, out Http2OutputFlowControlAwaitable awaitable)
|
||||||
|
{
|
||||||
|
var leastAvailableFlow = _connectionLevelFlowControl.Available < _streamLevelFlowControl.Available
|
||||||
|
? _connectionLevelFlowControl : _streamLevelFlowControl;
|
||||||
|
|
||||||
|
// Clamp ~= Math.Clamp from netcoreapp >= 2.0
|
||||||
|
var actual = Clamp(leastAvailableFlow.Available, 0, bytes);
|
||||||
|
|
||||||
|
// Make sure to advance prior to accessing AvailabilityAwaitable.
|
||||||
|
_connectionLevelFlowControl.Advance(actual);
|
||||||
|
_streamLevelFlowControl.Advance(actual);
|
||||||
|
|
||||||
|
awaitable = null;
|
||||||
|
_currentConnectionLevelAwaitable = null;
|
||||||
|
|
||||||
|
if (actual < bytes)
|
||||||
|
{
|
||||||
|
awaitable = leastAvailableFlow.AvailabilityAwaitable;
|
||||||
|
|
||||||
|
if (leastAvailableFlow == _connectionLevelFlowControl)
|
||||||
|
{
|
||||||
|
_currentConnectionLevelAwaitable = awaitable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return actual;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The connection-level update window is updated independently.
|
||||||
|
// https://httpwg.org/specs/rfc7540.html#rfc.section.6.9.1
|
||||||
|
public bool TryUpdateWindow(int bytes)
|
||||||
|
{
|
||||||
|
return _streamLevelFlowControl.TryUpdateWindow(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Abort()
|
||||||
|
{
|
||||||
|
_streamLevelFlowControl.Abort();
|
||||||
|
|
||||||
|
// If this stream is waiting on a connection-level window update, complete this stream's
|
||||||
|
// connection-level awaitable so the stream abort is observed immediately.
|
||||||
|
// This could complete an awaitable still sitting in the connection-level awaitable queue,
|
||||||
|
// but this is safe because completing it again will just no-op.
|
||||||
|
_currentConnectionLevelAwaitable?.Complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static int Clamp(int value, int min, long max)
|
||||||
|
{
|
||||||
|
Debug.Assert(min <= max, $"{nameof(Clamp)} called with a min greater than the max.");
|
||||||
|
|
||||||
|
if (value < min)
|
||||||
|
{
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
else if (value > max)
|
||||||
|
{
|
||||||
|
return (int)max;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,24 +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.Http;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|
||||||
{
|
|
||||||
public interface IHttp2FrameWriter
|
|
||||||
{
|
|
||||||
void Abort(Exception error);
|
|
||||||
Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken));
|
|
||||||
Task Write100ContinueAsync(int streamId);
|
|
||||||
void WriteResponseHeaders(int streamId, int statusCode, IHeaderDictionary headers);
|
|
||||||
Task WriteDataAsync(int streamId, ReadOnlySpan<byte> data, CancellationToken cancellationToken);
|
|
||||||
Task WriteDataAsync(int streamId, ReadOnlySpan<byte> data, bool endStream, CancellationToken cancellationToken);
|
|
||||||
Task WriteRstStreamAsync(int streamId, Http2ErrorCode errorCode);
|
|
||||||
Task WriteSettingsAckAsync();
|
|
||||||
Task WritePingAsync(Http2PingFrameFlags flags, ReadOnlySpan<byte> payload);
|
|
||||||
Task WriteGoAwayAsync(int lastStreamId, Http2ErrorCode errorCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
// 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.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Connections;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This wraps PipeWriter.FlushAsync() in a way that allows multiple awaiters making it safe to call from publicly
|
||||||
|
/// exposed Stream implementations.
|
||||||
|
/// </summary>
|
||||||
|
public class StreamSafePipeFlusher
|
||||||
|
{
|
||||||
|
private readonly PipeWriter _writer;
|
||||||
|
private readonly ITimeoutControl _timeoutControl;
|
||||||
|
private readonly object _flushLock = new object();
|
||||||
|
|
||||||
|
private Task _lastFlushTask = Task.CompletedTask;
|
||||||
|
|
||||||
|
public StreamSafePipeFlusher(
|
||||||
|
PipeWriter writer,
|
||||||
|
ITimeoutControl timeoutControl)
|
||||||
|
{
|
||||||
|
_writer = writer;
|
||||||
|
_timeoutControl = timeoutControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task FlushAsync(long count = 0, IHttpOutputProducer outputProducer = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var flushValueTask = _writer.FlushAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (flushValueTask.IsCompletedSuccessfully)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/dotnet/corefxlab/issues/1334
|
||||||
|
// Pipelines don't support multiple awaiters on flush.
|
||||||
|
// While it's acceptable to call PipeWriter.FlushAsync again before the last FlushAsync completes,
|
||||||
|
// it is not acceptable to attach a new continuation (via await, AsTask(), etc..). In this case,
|
||||||
|
// we find previous flush Task which still accounts for any newly committed bytes and await that.
|
||||||
|
lock (_flushLock)
|
||||||
|
{
|
||||||
|
if (_lastFlushTask.IsCompleted)
|
||||||
|
{
|
||||||
|
_lastFlushTask = flushValueTask.AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
return TimeFlushAsync(count, outputProducer, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TimeFlushAsync(long count, IHttpOutputProducer outputProducer, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_timeoutControl.StartTimingWrite(count);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _lastFlushTask;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException ex)
|
||||||
|
{
|
||||||
|
outputProducer.Abort(new ConnectionAbortedException(CoreStrings.ConnectionOrStreamAbortedByCancellationToken, ex));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// A canceled token is the only reason flush should ever throw.
|
||||||
|
}
|
||||||
|
|
||||||
|
_timeoutControl.StopTimingWrite();
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
// 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.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
|
{
|
||||||
|
public class ThreadPoolAwaitable : ICriticalNotifyCompletion
|
||||||
|
{
|
||||||
|
public static ThreadPoolAwaitable Instance = new ThreadPoolAwaitable();
|
||||||
|
|
||||||
|
private ThreadPoolAwaitable()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ThreadPoolAwaitable GetAwaiter() => this;
|
||||||
|
public bool IsCompleted => false;
|
||||||
|
|
||||||
|
public void GetResult()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnCompleted(Action continuation)
|
||||||
|
{
|
||||||
|
ThreadPool.QueueUserWorkItem(state => ((Action)state)(), continuation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnsafeOnCompleted(Action continuation)
|
||||||
|
{
|
||||||
|
OnCompleted(continuation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1904,6 +1904,90 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
||||||
internal static string FormatHttp2ErrorInvalidPreface()
|
internal static string FormatHttp2ErrorInvalidPreface()
|
||||||
=> GetString("Http2ErrorInvalidPreface");
|
=> GetString("Http2ErrorInvalidPreface");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The connection or stream was aborted because a write operation was aborted with a CancellationToken.
|
||||||
|
/// </summary>
|
||||||
|
internal static string ConnectionOrStreamAbortedByCancellationToken
|
||||||
|
{
|
||||||
|
get => GetString("ConnectionOrStreamAbortedByCancellationToken");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The connection or stream was aborted because a write operation was aborted with a CancellationToken.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatConnectionOrStreamAbortedByCancellationToken()
|
||||||
|
=> GetString("ConnectionOrStreamAbortedByCancellationToken");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The client sent a SETTINGS frame with a SETTINGS_INITIAL_WINDOW_SIZE that caused a flow-control window to exceed the maximum size.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Http2ErrorInitialWindowSizeInvalid
|
||||||
|
{
|
||||||
|
get => GetString("Http2ErrorInitialWindowSizeInvalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The client sent a SETTINGS frame with a SETTINGS_INITIAL_WINDOW_SIZE that caused a flow-control window to exceed the maximum size.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatHttp2ErrorInitialWindowSizeInvalid()
|
||||||
|
=> GetString("Http2ErrorInitialWindowSizeInvalid");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The client sent a WINDOW_UPDATE frame that caused a flow-control window to exceed the maximum size.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Http2ErrorWindowUpdateSizeInvalid
|
||||||
|
{
|
||||||
|
get => GetString("Http2ErrorWindowUpdateSizeInvalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The client sent a WINDOW_UPDATE frame that caused a flow-control window to exceed the maximum size.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatHttp2ErrorWindowUpdateSizeInvalid()
|
||||||
|
=> GetString("Http2ErrorWindowUpdateSizeInvalid");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The HTTP/2 connection faulted.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Http2ConnectionFaulted
|
||||||
|
{
|
||||||
|
get => GetString("Http2ConnectionFaulted");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The HTTP/2 connection faulted.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatHttp2ConnectionFaulted()
|
||||||
|
=> GetString("Http2ConnectionFaulted");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The client reset the request stream.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Http2StreamResetByClient
|
||||||
|
{
|
||||||
|
get => GetString("Http2StreamResetByClient");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The client reset the request stream.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatHttp2StreamResetByClient()
|
||||||
|
=> GetString("Http2StreamResetByClient");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The request stream was aborted.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Http2StreamAborted
|
||||||
|
{
|
||||||
|
get => GetString("Http2StreamAborted");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The request stream was aborted.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatHttp2StreamAborted()
|
||||||
|
=> GetString("Http2StreamAborted");
|
||||||
|
|
||||||
private static string GetString(string name, params string[] formatterNames)
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
{
|
{
|
||||||
var value = _resourceManager.GetString(name);
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -54,8 +54,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var dataset = new TheoryData<H2SpecTestCase>();
|
var dataset = new TheoryData<H2SpecTestCase>();
|
||||||
var toSkip = new[] { "hpack/4.2/1", "http2/5.1/8", "http2/6.9.1/2", "http2/6.9.1/3", "http2/8.1.2.3/1",
|
var toSkip = new[] { "hpack/4.2/1", "http2/5.1/8", "http2/8.1.2.3/1", "http2/8.1.2.6/1", "http2/8.1.2.6/2" };
|
||||||
"http2/8.1.2.6/1", "http2/8.1.2.6/2" };
|
|
||||||
|
|
||||||
foreach (var testcase in H2SpecCommands.EnumerateTestCases())
|
foreach (var testcase in H2SpecCommands.EnumerateTestCases())
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue