diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx b/src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx
index 16e59e4c3a..63f8986ab5 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx
@@ -342,4 +342,7 @@
Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead.
+
+ Value must be a positive number. To disable a minimum data rate, use null where a MinDataRate instance is expected.
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/IHttpMinRequestBodyDataRateFeature.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/IHttpMinRequestBodyDataRateFeature.cs
index af5ee4e66f..f80bd99772 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/IHttpMinRequestBodyDataRateFeature.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/IHttpMinRequestBodyDataRateFeature.cs
@@ -4,12 +4,12 @@
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
{
///
- /// Represents a minimum data rate for the request body of an HTTP request.
+ /// Feature to set the minimum data rate at which the the request body must be sent by the client.
///
public interface IHttpMinRequestBodyDataRateFeature
{
///
- /// The minimum data rate in bytes/second at which the request body should be received.
+ /// The minimum data rate in bytes/second at which the request body must be sent by the client.
/// Setting this property to null indicates no minimum data rate should be enforced.
/// This limit has no effect on upgraded connections which are always unlimited.
///
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/IHttpMinResponseDataRateFeature.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/IHttpMinResponseDataRateFeature.cs
new file mode 100644
index 0000000000..f901a338d9
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/IHttpMinResponseDataRateFeature.cs
@@ -0,0 +1,18 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
+{
+ ///
+ /// Feature to set the minimum data rate at which the response must be received by the client.
+ ///
+ public interface IHttpMinResponseDataRateFeature
+ {
+ ///
+ /// The minimum data rate in bytes/second at which the response must be received by the client.
+ /// Setting this property to null indicates no minimum data rate should be enforced.
+ /// This limit has no effect on upgraded connections which are always unlimited.
+ ///
+ MinDataRate MinDataRate { get; set; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/FrameConnection.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/FrameConnection.cs
index 6d053da559..1574d3bf7e 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/FrameConnection.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/FrameConnection.cs
@@ -7,8 +7,8 @@ using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
@@ -35,6 +35,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
private long _readTimingElapsedTicks;
private long _readTimingBytesRead;
+ private object _writeTimingLock = new object();
+ private int _writeTimingWrites;
+ private long _writeTimingTimeoutTimestamp;
+
private Task _lifetimeTask;
public FrameConnection(FrameConnectionContext context)
@@ -46,7 +50,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
internal Frame Frame => _frame;
internal IDebugger Debugger { get; set; } = DebuggerWrapper.Singleton;
-
public bool TimedOut { get; private set; }
public string ConnectionId => _context.ConnectionId;
@@ -207,7 +210,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
TimedOut = true;
- _readTimingEnabled = false;
_frame.Stop();
}
@@ -262,6 +264,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
var timestamp = now.Ticks;
+ CheckForTimeout(timestamp);
+ CheckForReadDataRateTimeout(timestamp);
+ CheckForWriteDataRateTimeout(timestamp);
+
+ Interlocked.Exchange(ref _lastTimestamp, timestamp);
+ }
+
+ private void CheckForTimeout(long timestamp)
+ {
+ if (TimedOut)
+ {
+ return;
+ }
+
// TODO: Use PlatformApis.VolatileRead equivalent again
if (timestamp > Interlocked.Read(ref _timeoutTimestamp))
{
@@ -277,42 +293,67 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
Timeout();
}
}
- else
+ }
+
+ private void CheckForReadDataRateTimeout(long timestamp)
+ {
+ // The only time when both a timeout is set and the read data rate could be enforced is
+ // when draining the request body. Since there's already a (short) timeout set for draining,
+ // it's safe to not check the data rate at this point.
+ if (TimedOut || Interlocked.Read(ref _timeoutTimestamp) != long.MaxValue)
{
- lock (_readTimingLock)
+ return;
+ }
+
+ lock (_readTimingLock)
+ {
+ if (_readTimingEnabled)
{
- if (_readTimingEnabled)
+ // Reference in local var to avoid torn reads in case the min rate is changed via IHttpMinRequestBodyDataRateFeature
+ var minRequestBodyDataRate = _frame.MinRequestBodyDataRate;
+
+ _readTimingElapsedTicks += timestamp - _lastTimestamp;
+
+ if (minRequestBodyDataRate?.BytesPerSecond > 0 && _readTimingElapsedTicks > minRequestBodyDataRate.GracePeriod.Ticks)
{
- // Reference in local var to avoid torn reads in case the min rate is changed via IHttpMinRequestBodyDataRateFeature
- var minRequestBodyDataRate = _frame.MinRequestBodyDataRate;
+ var elapsedSeconds = (double)_readTimingElapsedTicks / TimeSpan.TicksPerSecond;
+ var rate = Interlocked.Read(ref _readTimingBytesRead) / elapsedSeconds;
- _readTimingElapsedTicks += timestamp - _lastTimestamp;
-
- if (minRequestBodyDataRate?.BytesPerSecond > 0 && _readTimingElapsedTicks > minRequestBodyDataRate.GracePeriod.Ticks)
+ if (rate < minRequestBodyDataRate.BytesPerSecond && !Debugger.IsAttached)
{
- var elapsedSeconds = (double)_readTimingElapsedTicks / TimeSpan.TicksPerSecond;
- var rate = Interlocked.Read(ref _readTimingBytesRead) / elapsedSeconds;
-
- if (rate < minRequestBodyDataRate.BytesPerSecond && !Debugger.IsAttached)
- {
- Log.RequestBodyMininumDataRateNotSatisfied(_context.ConnectionId, _frame.TraceIdentifier, minRequestBodyDataRate.BytesPerSecond);
- Timeout();
- }
+ Log.RequestBodyMininumDataRateNotSatisfied(_context.ConnectionId, _frame.TraceIdentifier, minRequestBodyDataRate.BytesPerSecond);
+ Timeout();
}
+ }
- // PauseTimingReads() cannot just set _timingReads to false. It needs to go through at least one tick
- // before pausing, otherwise _readTimingElapsed might never be updated if PauseTimingReads() is always
- // called before the next tick.
- if (_readTimingPauseRequested)
- {
- _readTimingEnabled = false;
- _readTimingPauseRequested = false;
- }
+ // PauseTimingReads() cannot just set _timingReads to false. It needs to go through at least one tick
+ // before pausing, otherwise _readTimingElapsed might never be updated if PauseTimingReads() is always
+ // called before the next tick.
+ if (_readTimingPauseRequested)
+ {
+ _readTimingEnabled = false;
+ _readTimingPauseRequested = false;
}
}
}
+ }
- Interlocked.Exchange(ref _lastTimestamp, timestamp);
+ private void CheckForWriteDataRateTimeout(long timestamp)
+ {
+ if (TimedOut)
+ {
+ return;
+ }
+
+ lock (_writeTimingLock)
+ {
+ if (_writeTimingWrites > 0 && timestamp > _writeTimingTimeoutTimestamp && !Debugger.IsAttached)
+ {
+ TimedOut = true;
+ Log.ResponseMininumDataRateNotSatisfied(_frame.ConnectionIdFeature, _frame.TraceIdentifier);
+ Abort(new TimeoutException());
+ }
+ }
}
public void SetTimeout(long ticks, TimeoutAction timeoutAction)
@@ -381,5 +422,37 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{
Interlocked.Add(ref _readTimingBytesRead, count);
}
+
+ public void StartTimingWrite(int size)
+ {
+ lock (_writeTimingLock)
+ {
+ var minResponseDataRate = _frame.MinResponseDataRate;
+
+ if (minResponseDataRate != null)
+ {
+ var timeoutTicks = Math.Max(
+ minResponseDataRate.GracePeriod.Ticks,
+ TimeSpan.FromSeconds(size / minResponseDataRate.BytesPerSecond).Ticks);
+
+ if (_writeTimingWrites == 0)
+ {
+ // Add Heartbeat.Interval since this can be called right before the next heartbeat.
+ _writeTimingTimeoutTimestamp = _lastTimestamp + Heartbeat.Interval.Ticks;
+ }
+
+ _writeTimingTimeoutTimestamp += timeoutTicks;
+ _writeTimingWrites++;
+ }
+ }
+ }
+
+ public void StopTimingWrite()
+ {
+ lock (_writeTimingLock)
+ {
+ _writeTimingWrites--;
+ }
+ }
}
}
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.FeatureCollection.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.FeatureCollection.cs
index cbc30427e5..3cf9d22fac 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.FeatureCollection.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.FeatureCollection.cs
@@ -24,7 +24,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
IHttpRequestIdentifierFeature,
IHttpBodyControlFeature,
IHttpMaxRequestBodySizeFeature,
- IHttpMinRequestBodyDataRateFeature
+ IHttpMinRequestBodyDataRateFeature,
+ IHttpMinResponseDataRateFeature
{
// NOTE: When feature interfaces are added to or removed from this Frame class implementation,
// then the list of `implementedFeatures` in the generated code project MUST also be updated.
@@ -242,6 +243,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
set => MinRequestBodyDataRate = value;
}
+ MinDataRate IHttpMinResponseDataRateFeature.MinDataRate
+ {
+ get => MinResponseDataRate;
+ set => MinResponseDataRate = value;
+ }
+
object IFeatureCollection.this[Type key]
{
get => FastFeatureGet(key);
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.Generated.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.Generated.cs
index 6aba909708..c4403a60ce 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.Generated.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.Generated.cs
@@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private static readonly Type ISessionFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.ISessionFeature);
private static readonly Type IHttpMaxRequestBodySizeFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature);
private static readonly Type IHttpMinRequestBodyDataRateFeatureType = typeof(global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpMinRequestBodyDataRateFeature);
+ private static readonly Type IHttpMinResponseDataRateFeatureType = typeof(global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpMinResponseDataRateFeature);
private static readonly Type IHttpBodyControlFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature);
private static readonly Type IHttpSendFileFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature);
@@ -45,6 +46,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private object _currentISessionFeature;
private object _currentIHttpMaxRequestBodySizeFeature;
private object _currentIHttpMinRequestBodyDataRateFeature;
+ private object _currentIHttpMinResponseDataRateFeature;
private object _currentIHttpBodyControlFeature;
private object _currentIHttpSendFileFeature;
@@ -58,6 +60,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_currentIHttpConnectionFeature = this;
_currentIHttpMaxRequestBodySizeFeature = this;
_currentIHttpMinRequestBodyDataRateFeature = this;
+ _currentIHttpMinResponseDataRateFeature = this;
_currentIHttpBodyControlFeature = this;
_currentIServiceProvidersFeature = null;
@@ -142,6 +145,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
return _currentIHttpMinRequestBodyDataRateFeature;
}
+ if (key == IHttpMinResponseDataRateFeatureType)
+ {
+ return _currentIHttpMinResponseDataRateFeature;
+ }
if (key == IHttpBodyControlFeatureType)
{
return _currentIHttpBodyControlFeature;
@@ -242,6 +249,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_currentIHttpMinRequestBodyDataRateFeature = feature;
return;
}
+ if (key == IHttpMinResponseDataRateFeatureType)
+ {
+ _currentIHttpMinResponseDataRateFeature = feature;
+ return;
+ }
if (key == IHttpBodyControlFeatureType)
{
_currentIHttpBodyControlFeature = feature;
@@ -325,6 +337,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
yield return new KeyValuePair(IHttpMinRequestBodyDataRateFeatureType, _currentIHttpMinRequestBodyDataRateFeature as global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpMinRequestBodyDataRateFeature);
}
+ if (_currentIHttpMinResponseDataRateFeature != null)
+ {
+ yield return new KeyValuePair(IHttpMinResponseDataRateFeatureType, _currentIHttpMinResponseDataRateFeature as global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpMinResponseDataRateFeature);
+ }
if (_currentIHttpBodyControlFeature != null)
{
yield return new KeyValuePair(IHttpBodyControlFeatureType, _currentIHttpBodyControlFeature as global::Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature);
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs
index c9e7ba118b..e6b1187438 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs
@@ -97,7 +97,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_keepAliveTicks = ServerOptions.Limits.KeepAliveTimeout.Ticks;
_requestHeadersTimeoutTicks = ServerOptions.Limits.RequestHeadersTimeout.Ticks;
- Output = new OutputProducer(frameContext.Output, frameContext.ConnectionId, frameContext.ServiceContext.Log);
+ Output = new OutputProducer(frameContext.Output, frameContext.ConnectionId, frameContext.ServiceContext.Log, TimeoutControl);
RequestBodyPipe = CreateRequestBodyPipe();
}
@@ -302,6 +302,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public MinDataRate MinRequestBodyDataRate { get; set; }
+ public MinDataRate MinResponseDataRate { get; set; }
+
public void InitializeStreams(MessageBody messageBody)
{
if (_frameStreams == null)
@@ -381,6 +383,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_requestCount++;
MinRequestBodyDataRate = ServerOptions.Limits.MinRequestBodyDataRate;
+ MinResponseDataRate = ServerOptions.Limits.MinResponseDataRate;
}
///
@@ -418,7 +421,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_frameStreams?.Abort(error);
- Output.Abort();
+ Output.Abort(error);
// Potentially calling user code. CancelRequestAbortedToken logs any exceptions.
ServiceContext.ThreadPool.UnsafeRun(state => ((Frame)state).CancelRequestAbortedToken(), this);
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/OutputProducer.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/OutputProducer.cs
index 5f58ef6ae4..d9fce78492 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/OutputProducer.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/OutputProducer.cs
@@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private static readonly ArraySegment _emptyData = new ArraySegment(new byte[0]);
private readonly string _connectionId;
+ private readonly ITimeoutControl _timeoutControl;
private readonly IKestrelTrace _log;
// This locks access to to all of the below fields
@@ -30,10 +31,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private readonly object _flushLock = new object();
private Action _flushCompleted;
- public OutputProducer(IPipe pipe, string connectionId, IKestrelTrace log)
+ public OutputProducer(
+ IPipe pipe,
+ string connectionId,
+ IKestrelTrace log,
+ ITimeoutControl timeoutControl)
{
_pipe = pipe;
_connectionId = connectionId;
+ _timeoutControl = timeoutControl;
_log = log;
_flushCompleted = OnFlushCompleted;
}
@@ -83,7 +89,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
- public void Abort()
+ public void Abort(Exception error)
{
lock (_contextLock)
{
@@ -94,8 +100,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_log.ConnectionDisconnect(_connectionId);
_completed = true;
+
_pipe.Reader.CancelPendingRead();
- _pipe.Writer.Complete();
+ _pipe.Writer.Complete(error);
}
}
@@ -145,10 +152,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
// The flush task can't fail today
return Task.CompletedTask;
}
- return FlushAsyncAwaited(awaitable, cancellationToken);
+ return FlushAsyncAwaited(awaitable, writableBuffer.BytesWritten, cancellationToken);
}
- private async Task FlushAsyncAwaited(WritableBufferAwaitable awaitable, CancellationToken cancellationToken)
+ private async Task FlushAsyncAwaited(WritableBufferAwaitable awaitable, int count, CancellationToken cancellationToken)
{
// https://github.com/dotnet/corefxlab/issues/1334
// Since the flush awaitable doesn't currently support multiple awaiters
@@ -163,7 +170,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
awaitable.OnCompleted(_flushCompleted);
}
}
+
+ _timeoutControl.StartTimingWrite(count);
await _flushTcs.Task;
+ _timeoutControl.StopTimingWrite();
cancellationToken.ThrowIfCancellationRequested();
}
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs
index b7d68eb03a..456e132274 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs
@@ -43,5 +43,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
void RequestBodyDone(string connectionId, string traceIdentifier);
void RequestBodyMininumDataRateNotSatisfied(string connectionId, string traceIdentifier, double rate);
+
+ void ResponseMininumDataRateNotSatisfied(string connectionId, string traceIdentifier);
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/ITimeoutControl.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/ITimeoutControl.cs
index 6b23f37c94..4ed025ac72 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/ITimeoutControl.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/ITimeoutControl.cs
@@ -16,5 +16,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
void ResumeTimingReads();
void StopTimingReads();
void BytesRead(int count);
+
+ void StartTimingWrite(int size);
+ void StopTimingWrite();
}
}
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs
index 115d44184d..ab6eb51293 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs
@@ -61,7 +61,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
LoggerMessage.Define(LogLevel.Debug, 26, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": done reading request body.");
private static readonly Action _requestBodyMinimumDataRateNotSatisfied =
- LoggerMessage.Define(LogLevel.Information, 27, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": request body incoming data rate dropped below {Rate} bytes/second.");
+ LoggerMessage.Define(LogLevel.Information, 27, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": the request timed out because it was not sent by the client at a minimum of {Rate} bytes/second.");
+
+ private static readonly Action _responseMinimumDataRateNotSatisfied =
+ LoggerMessage.Define(LogLevel.Information, 28, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": the connection was closed becuase the response was not read by the client at the specified minimum data rate.");
protected readonly ILogger _logger;
@@ -160,6 +163,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
_requestBodyMinimumDataRateNotSatisfied(_logger, connectionId, traceIdentifier, rate, null);
}
+ public void ResponseMininumDataRateNotSatisfied(string connectionId, string traceIdentifier)
+ {
+ _responseMinimumDataRateNotSatisfied(_logger, connectionId, traceIdentifier, null);
+ }
+
public virtual void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
=> _logger.Log(logLevel, eventId, state, exception, formatter);
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServerLimits.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServerLimits.cs
index 528f78be57..eb061cf500 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServerLimits.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServerLimits.cs
@@ -263,5 +263,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
public MinDataRate MinRequestBodyDataRate { get; set; } =
// Matches the default IIS minBytesPerSecond
new MinDataRate(bytesPerSecond: 240, gracePeriod: TimeSpan.FromSeconds(5));
+
+ ///
+ /// Gets or sets the response minimum data rate in bytes/second.
+ /// Setting this property to null indicates no minimum data rate should be enforced.
+ /// This limit has no effect on upgraded connections which are always unlimited.
+ /// This can be overridden per-request via .
+ ///
+ ///
+ ///
+ /// Defaults to 240 bytes/second with a 5 second grace period.
+ ///
+ ///
+ /// Contrary to the request body minimum data rate, this rate applies to the response status line and headers as well.
+ ///
+ ///
+ /// This rate is enforced per write operation instead of being averaged over the life of the response. Whenever the server
+ /// writes a chunk of data, a timer is set to the maximum of the grace period set in this property or the length of the write in
+ /// bytes divided by the data rate (i.e. the maximum amount of time that write should take to complete with the specified data rate).
+ /// The connection is aborted if the write has not completed by the time that timer expires.
+ ///
+ ///
+ public MinDataRate MinResponseDataRate { get; set; } =
+ // Matches the default IIS minBytesPerSecond
+ new MinDataRate(bytesPerSecond: 240, gracePeriod: TimeSpan.FromSeconds(5));
}
}
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/MinDataRate.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/MinDataRate.cs
index 34cbdc577d..0e320b37f1 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/MinDataRate.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/MinDataRate.cs
@@ -16,9 +16,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// starting at the time data is first read or written.
public MinDataRate(double bytesPerSecond, TimeSpan gracePeriod)
{
- if (bytesPerSecond < 0)
+ if (bytesPerSecond <= 0)
{
- throw new ArgumentOutOfRangeException(nameof(bytesPerSecond), CoreStrings.NonNegativeNumberRequired);
+ throw new ArgumentOutOfRangeException(nameof(bytesPerSecond), CoreStrings.PositiveNumberOrNullMinDataRateRequired);
}
if (gracePeriod <= Heartbeat.Interval)
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Properties/CoreStrings.Designer.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Properties/CoreStrings.Designer.cs
index aa43e6b644..4598adc6fa 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Properties/CoreStrings.Designer.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Properties/CoreStrings.Designer.cs
@@ -1060,6 +1060,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatSynchronousWritesDisallowed()
=> GetString("SynchronousWritesDisallowed");
+ ///
+ /// Value must be a positive number. To disable a minimum data rate, use null where a MinDataRate instance is expected.
+ ///
+ internal static string PositiveNumberOrNullMinDataRateRequired
+ {
+ get => GetString("PositiveNumberOrNullMinDataRateRequired");
+ }
+
+ ///
+ /// Value must be a positive number. To disable a minimum data rate, use null where a MinDataRate instance is expected.
+ ///
+ internal static string FormatPositiveNumberOrNullMinDataRateRequired()
+ => GetString("PositiveNumberOrNullMinDataRateRequired");
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvAwaitable.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvAwaitable.cs
index 7029a1c32e..8ee11ff42e 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvAwaitable.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvAwaitable.cs
@@ -79,4 +79,4 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
Error = error;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvConnection.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvConnection.cs
index 2052ac45f2..2bac70080e 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvConnection.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvConnection.cs
@@ -60,28 +60,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
StartReading();
+ Exception error = null;
+
try
{
// This *must* happen after socket.ReadStart
// The socket output consumer is the only thing that can close the connection. If the
// output pipe is already closed by the time we start then it's fine since, it'll close gracefully afterwards.
await Output.WriteOutputAsync();
-
- // Now, complete the input so that no more reads can happen
- Input.Complete(new ConnectionAbortedException());
- _connectionContext.Output.Complete();
- _connectionContext.OnConnectionClosed(ex: null);
}
catch (UvException ex)
{
- var ioEx = new IOException(ex.Message, ex);
-
- Input.Complete(ioEx);
- _connectionContext.Output.Complete(ioEx);
- _connectionContext.OnConnectionClosed(ioEx);
+ error = new IOException(ex.Message, ex);
}
finally
{
+ // Now, complete the input so that no more reads can happen
+ Input.Complete(error ?? new ConnectionAbortedException());
+ _connectionContext.Output.Complete(error);
+ _connectionContext.OnConnectionClosed(error);
+
// Make sure it isn't possible for a paused read to resume reading after calling uv_close
// on the stream handle
Input.CancelPendingFlush();
diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvOutputConsumer.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvOutputConsumer.cs
index c49b5bde5b..957ceb9595 100644
--- a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvOutputConsumer.cs
+++ b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvOutputConsumer.cs
@@ -4,7 +4,6 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines;
-using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networking;
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
@@ -29,6 +28,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
_socket = socket;
_connectionId = connectionId;
_log = log;
+
+ _pipe.OnWriterCompleted(OnWriterCompleted, this);
}
public async Task WriteOutputAsync()
@@ -37,7 +38,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
while (true)
{
- var result = await _pipe.ReadAsync();
+ ReadResult result;
+
+ try
+ {
+ result = await _pipe.ReadAsync();
+ }
+ catch
+ {
+ // Handled in OnWriterCompleted
+ return;
+ }
+
var buffer = result.Buffer;
var consumed = buffer.End;
@@ -54,6 +66,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
try
{
+ if (_socket.IsClosed)
+ {
+ break;
+ }
+
var writeResult = await writeReq.WriteAsync(_socket, buffer);
LogWriteInfo(writeResult.Status, writeResult.Error);
@@ -82,6 +99,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
}
}
+ private static void OnWriterCompleted(Exception ex, object state)
+ {
+ // Cut off writes if the writer is completed with an error. If a write request is pending, this will cancel it.
+ if (ex != null)
+ {
+ var libuvOutputConsumer = (LibuvOutputConsumer)state;
+ libuvOutputConsumer._socket.Dispose();
+ }
+ }
+
private void LogWriteInfo(int status, Exception error)
{
if (error == null)
diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameConnectionTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameConnectionTests.cs
index 790ce56d7e..8c1d74eb24 100644
--- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameConnectionTests.cs
+++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameConnectionTests.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
@@ -129,7 +130,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var mockLogger = new Mock();
_frameConnectionContext.ServiceContext.Log = mockLogger.Object;
- _frameConnection.CreateFrame(new DummyApplication(context => Task.CompletedTask), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
+ _frameConnection.CreateFrame(new DummyApplication(), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
_frameConnection.Frame.Reset();
// Initialize timestamp
@@ -171,7 +172,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var mockLogger = new Mock();
_frameConnectionContext.ServiceContext.Log = mockLogger.Object;
- _frameConnection.CreateFrame(new DummyApplication(context => Task.CompletedTask), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
+ _frameConnection.CreateFrame(new DummyApplication(), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
_frameConnection.Frame.Reset();
// Initialize timestamp
@@ -248,7 +249,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var mockLogger = new Mock();
_frameConnectionContext.ServiceContext.Log = mockLogger.Object;
- _frameConnection.CreateFrame(new DummyApplication(context => Task.CompletedTask), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
+ _frameConnection.CreateFrame(new DummyApplication(), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
_frameConnection.Frame.Reset();
// Initialize timestamp
@@ -316,7 +317,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var mockLogger = new Mock();
_frameConnectionContext.ServiceContext.Log = mockLogger.Object;
- _frameConnection.CreateFrame(new DummyApplication(context => Task.CompletedTask), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
+ _frameConnection.CreateFrame(new DummyApplication(), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
_frameConnection.Frame.Reset();
// Initialize timestamp
@@ -364,5 +365,169 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), It.IsAny()),
Times.Once);
}
+
+ [Fact]
+ public void ReadTimingNotEnforcedWhenTimeoutIsSet()
+ {
+ var systemClock = new MockSystemClock();
+ var timeout = TimeSpan.FromSeconds(5);
+
+ _frameConnectionContext.ServiceContext.ServerOptions.Limits.MinRequestBodyDataRate =
+ new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2));
+ _frameConnectionContext.ServiceContext.SystemClock = systemClock;
+
+ var mockLogger = new Mock();
+ _frameConnectionContext.ServiceContext.Log = mockLogger.Object;
+
+ _frameConnection.CreateFrame(new DummyApplication(), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
+ _frameConnection.Frame.Reset();
+
+ var startTime = systemClock.UtcNow;
+
+ // Initialize timestamp
+ _frameConnection.Tick(startTime);
+
+ _frameConnection.StartTimingReads();
+
+ _frameConnection.SetTimeout(timeout.Ticks, TimeoutAction.CloseConnection);
+
+ // Tick beyond grace period with low data rate
+ systemClock.UtcNow += TimeSpan.FromSeconds(3);
+ _frameConnection.BytesRead(1);
+ _frameConnection.Tick(systemClock.UtcNow);
+
+ // Not timed out
+ Assert.False(_frameConnection.TimedOut);
+
+ // Tick just past timeout period, adjusted by Heartbeat.Interval
+ systemClock.UtcNow = startTime + timeout + Heartbeat.Interval + TimeSpan.FromTicks(1);
+ _frameConnection.Tick(systemClock.UtcNow);
+
+ // Timed out
+ Assert.True(_frameConnection.TimedOut);
+ }
+
+ [Fact]
+ public void WriteTimingAbortsConnectionWhenWriteDoesNotCompleteWithMinimumDataRate()
+ {
+ var systemClock = new MockSystemClock();
+ var aborted = new ManualResetEventSlim();
+
+ _frameConnectionContext.ServiceContext.ServerOptions.Limits.MinResponseDataRate =
+ new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2));
+ _frameConnectionContext.ServiceContext.SystemClock = systemClock;
+
+ var mockLogger = new Mock();
+ _frameConnectionContext.ServiceContext.Log = mockLogger.Object;
+
+ _frameConnection.CreateFrame(new DummyApplication(), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
+ _frameConnection.Frame.Reset();
+ _frameConnection.Frame.RequestAborted.Register(() =>
+ {
+ aborted.Set();
+ });
+
+ // Initialize timestamp
+ _frameConnection.Tick(systemClock.UtcNow);
+
+ // Should complete within 4 seconds, but the timeout is adjusted by adding Heartbeat.Interval
+ _frameConnection.StartTimingWrite(400);
+
+ // Tick just past 4s plus Heartbeat.Interval
+ systemClock.UtcNow += TimeSpan.FromSeconds(4) + Heartbeat.Interval + TimeSpan.FromTicks(1);
+ _frameConnection.Tick(systemClock.UtcNow);
+
+ Assert.True(_frameConnection.TimedOut);
+ Assert.True(aborted.Wait(TimeSpan.FromSeconds(10)));
+ }
+
+ [Fact]
+ public void WriteTimingAbortsConnectionWhenSmallWriteDoesNotCompleteWithinGracePeriod()
+ {
+ var systemClock = new MockSystemClock();
+ var minResponseDataRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(5));
+ var aborted = new ManualResetEventSlim();
+
+ _frameConnectionContext.ServiceContext.ServerOptions.Limits.MinResponseDataRate = minResponseDataRate;
+ _frameConnectionContext.ServiceContext.SystemClock = systemClock;
+
+ var mockLogger = new Mock();
+ _frameConnectionContext.ServiceContext.Log = mockLogger.Object;
+
+ _frameConnection.CreateFrame(new DummyApplication(), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
+ _frameConnection.Frame.Reset();
+ _frameConnection.Frame.RequestAborted.Register(() =>
+ {
+ aborted.Set();
+ });
+
+ // Initialize timestamp
+ var startTime = systemClock.UtcNow;
+ _frameConnection.Tick(startTime);
+
+ // Should complete within 1 second, but the timeout is adjusted by adding Heartbeat.Interval
+ _frameConnection.StartTimingWrite(100);
+
+ // Tick just past 1s plus Heartbeat.Interval
+ systemClock.UtcNow += TimeSpan.FromSeconds(1) + Heartbeat.Interval + TimeSpan.FromTicks(1);
+ _frameConnection.Tick(systemClock.UtcNow);
+
+ // Still within grace period, not timed out
+ Assert.False(_frameConnection.TimedOut);
+
+ // Tick just past grace period (adjusted by Heartbeat.Interval)
+ systemClock.UtcNow = startTime + minResponseDataRate.GracePeriod + Heartbeat.Interval + TimeSpan.FromTicks(1);
+ _frameConnection.Tick(systemClock.UtcNow);
+
+ Assert.True(_frameConnection.TimedOut);
+ Assert.True(aborted.Wait(TimeSpan.FromSeconds(10)));
+ }
+
+ [Fact]
+ public void WriteTimingTimeoutPushedOnConcurrentWrite()
+ {
+ var systemClock = new MockSystemClock();
+ var aborted = new ManualResetEventSlim();
+
+ _frameConnectionContext.ServiceContext.ServerOptions.Limits.MinResponseDataRate =
+ new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2));
+ _frameConnectionContext.ServiceContext.SystemClock = systemClock;
+
+ var mockLogger = new Mock();
+ _frameConnectionContext.ServiceContext.Log = mockLogger.Object;
+
+ _frameConnection.CreateFrame(new DummyApplication(), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
+ _frameConnection.Frame.Reset();
+ _frameConnection.Frame.RequestAborted.Register(() =>
+ {
+ aborted.Set();
+ });
+
+ // Initialize timestamp
+ _frameConnection.Tick(systemClock.UtcNow);
+
+ // Should complete within 5 seconds, but the timeout is adjusted by adding Heartbeat.Interval
+ _frameConnection.StartTimingWrite(500);
+
+ // Start a concurrent write after 3 seconds, which should complete within 3 seconds (adjusted by Heartbeat.Interval)
+ _frameConnection.StartTimingWrite(300);
+
+ // Tick just past 5s plus Heartbeat.Interval, when the first write should have completed
+ systemClock.UtcNow += TimeSpan.FromSeconds(5) + Heartbeat.Interval + TimeSpan.FromTicks(1);
+ _frameConnection.Tick(systemClock.UtcNow);
+
+ // Not timed out because the timeout was pushed by the second write
+ Assert.False(_frameConnection.TimedOut);
+
+ // Complete the first write, this should have no effect on the timeout
+ _frameConnection.StopTimingWrite();
+
+ // Tick just past +3s, when the second write should have completed
+ systemClock.UtcNow += TimeSpan.FromSeconds(3) + TimeSpan.FromTicks(1);
+ _frameConnection.Tick(systemClock.UtcNow);
+
+ Assert.True(_frameConnection.TimedOut);
+ Assert.True(aborted.Wait(TimeSpan.FromSeconds(10)));
+ }
}
}
diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameTests.cs
index bd368df0c6..9531abb48e 100644
--- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameTests.cs
+++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameTests.cs
@@ -147,7 +147,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_frame.Reset();
- Assert.Equal(_serviceContext.ServerOptions.Limits.MinRequestBodyDataRate, _frame.MinRequestBodyDataRate);
+ Assert.Same(_serviceContext.ServerOptions.Limits.MinRequestBodyDataRate, _frame.MinRequestBodyDataRate);
+ }
+
+ [Fact]
+ public void ResetResetsMinResponseDataRate()
+ {
+ _frame.MinResponseDataRate = new MinDataRate(bytesPerSecond: 1, gracePeriod: TimeSpan.MaxValue);
+
+ _frame.Reset();
+
+ Assert.Same(_serviceContext.ServerOptions.Limits.MinResponseDataRate, _frame.MinResponseDataRate);
}
[Fact]
@@ -254,7 +264,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
[Theory]
- [MemberData(nameof(MinRequestBodyDataRateData))]
+ [MemberData(nameof(MinDataRateData))]
public void ConfiguringIHttpMinRequestBodyDataRateFeatureSetsMinRequestBodyDataRate(MinDataRate minDataRate)
{
((IFeatureCollection)_frame).Get().MinDataRate = minDataRate;
@@ -262,6 +272,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Same(minDataRate, _frame.MinRequestBodyDataRate);
}
+ [Theory]
+ [MemberData(nameof(MinDataRateData))]
+ public void ConfiguringIHttpMinResponseDataRateFeatureSetsMinResponseDataRate(MinDataRate minDataRate)
+ {
+ ((IFeatureCollection)_frame).Get().MinDataRate = minDataRate;
+
+ Assert.Same(minDataRate, _frame.MinResponseDataRate);
+ }
+
[Fact]
public void ResetResetsRequestHeaders()
{
@@ -878,7 +897,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
TimeSpan.Zero
};
- public static TheoryData MinRequestBodyDataRateData => new TheoryData
+ public static TheoryData MinDataRateData => new TheoryData
{
null,
new MinDataRate(bytesPerSecond: 1, gracePeriod: TimeSpan.MaxValue)
diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerLimitsTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerLimitsTests.cs
index bab86a04e7..66de5d41e5 100644
--- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerLimitsTests.cs
+++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerLimitsTests.cs
@@ -300,6 +300,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Equal(TimeSpan.FromSeconds(5), new KestrelServerLimits().MinRequestBodyDataRate.GracePeriod);
}
+ [Fact]
+ public void MinResponseBodyDataRateDefault()
+ {
+ Assert.NotNull(new KestrelServerLimits().MinResponseDataRate);
+ Assert.Equal(240, new KestrelServerLimits().MinResponseDataRate.BytesPerSecond);
+ Assert.Equal(TimeSpan.FromSeconds(5), new KestrelServerLimits().MinResponseDataRate.GracePeriod);
+ }
+
public static TheoryData TimeoutValidData => new TheoryData
{
TimeSpan.FromTicks(1),
diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MinDataRateTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MinDataRateTests.cs
index bd21c01f6d..a87bfa7709 100644
--- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MinDataRateTests.cs
+++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MinDataRateTests.cs
@@ -10,7 +10,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
public class MinDataRateTests
{
[Theory]
- [InlineData(0)]
[InlineData(double.Epsilon)]
[InlineData(double.MaxValue)]
public void BytesPerSecondValid(double value)
@@ -21,12 +20,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Theory]
[InlineData(double.MinValue)]
[InlineData(-double.Epsilon)]
+ [InlineData(0)]
public void BytesPerSecondInvalid(double value)
{
var exception = Assert.Throws(() => new MinDataRate(bytesPerSecond: value, gracePeriod: TimeSpan.MaxValue));
Assert.Equal("bytesPerSecond", exception.ParamName);
- Assert.StartsWith(CoreStrings.NonNegativeNumberRequired, exception.Message);
+ Assert.StartsWith(CoreStrings.PositiveNumberOrNullMinDataRateRequired, exception.Message);
}
[Theory]
diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/OutputProducerTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/OutputProducerTests.cs
index 0298bbff95..027e26f44e 100644
--- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/OutputProducerTests.cs
+++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/OutputProducerTests.cs
@@ -54,16 +54,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
var pipe = _pipeFactory.Create(pipeOptions);
var serviceContext = new TestServiceContext();
- var frameContext = new FrameContext
- {
- ServiceContext = serviceContext,
- ConnectionInformation = new MockConnectionInformation
- {
- PipeFactory = _pipeFactory
- }
- };
- var frame = new Frame