Skip to content

Commit 8d83e53

Browse files
authored
Disable request body data rate limits per HTTP/2 stream (#10355)
1 parent 2e89aa4 commit 8d83e53

19 files changed

+130
-35
lines changed

src/Servers/Kestrel/Core/src/CoreStrings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,4 +599,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
599599
<data name="StartAsyncBeforeGetMemory" xml:space="preserve">
600600
<value>Cannot call GetMemory() until response has started. Call HttpResponse.StartAsync() before calling GetMemory().</value>
601601
</data>
602+
<data name="Http2MinDataRateNotSupported" xml:space="preserve">
603+
<value>This feature is not supported for HTTP/2 requests except to disable it entirely by setting the rate to null.</value>
604+
</data>
602605
</root>

src/Servers/Kestrel/Core/src/Features/IHttpMinRequestBodyDataRateFeature.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
55
{
66
/// <summary>
77
/// Feature to set the minimum data rate at which the the request body must be sent by the client.
8-
/// This feature is not available for HTTP/2 requests. Instead, use <see cref="KestrelServerLimits.MinRequestBodyDataRate"/>
9-
/// for server-wide configuration which applies to both HTTP/2 and HTTP/1.x.
8+
/// This feature is not supported for HTTP/2 requests except to disable it entirely by setting <see cref="MinDataRate"/> to <see langword="null"/>
9+
/// Instead, use <see cref="KestrelServerLimits.MinRequestBodyDataRate"/> for server-wide configuration which applies to both HTTP/2 and HTTP/1.x.
1010
/// </summary>
1111
public interface IHttpMinRequestBodyDataRateFeature
1212
{
1313
/// <summary>
1414
/// The minimum data rate in bytes/second at which the request body must be sent by the client.
1515
/// Setting this property to null indicates no minimum data rate should be enforced.
1616
/// This limit has no effect on upgraded connections which are always unlimited.
17-
/// This feature is not available for HTTP/2 requests. Instead, use <see cref="KestrelServerLimits.MinRequestBodyDataRate"/>
18-
/// for server-wide configuration which applies to both HTTP/2 and HTTP/1.x.
17+
/// This feature is not supported for HTTP/2 requests except to disable it entirely by setting <see cref="MinDataRate"/> to <see langword="null"/>
18+
/// Instead, use <see cref="KestrelServerLimits.MinRequestBodyDataRate"/> for server-wide configuration which applies to both HTTP/2 and HTTP/1.x.
1919
/// </summary>
2020
MinDataRate MinDataRate { get; set; }
2121
}

src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.FeatureCollection.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using System;
5-
using System.IO;
6-
using System.Threading;
7-
using System.Threading.Tasks;
8-
using Microsoft.AspNetCore.Http;
9-
using Microsoft.AspNetCore.Http.Features;
104
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
115

126
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
137
{
148
internal partial class Http1Connection : IHttpMinRequestBodyDataRateFeature,
15-
IHttpMinResponseDataRateFeature
9+
IHttpMinResponseDataRateFeature
1610
{
1711
MinDataRate IHttpMinRequestBodyDataRateFeature.MinDataRate
1812
{

src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,6 @@ public Http1Connection(HttpConnectionContext context)
6262

6363
public bool RequestTimedOut => _requestTimedOut;
6464

65-
public MinDataRate MinRequestBodyDataRate { get; set; }
66-
6765
public MinDataRate MinResponseDataRate { get; set; }
6866

6967
public MemoryPool<byte> MemoryPool { get; }
@@ -542,7 +540,6 @@ protected override void OnReset()
542540
_remainingRequestHeadersBytesAllowed = ServerOptions.Limits.MaxRequestHeadersTotalSize + 2;
543541
_requestCount++;
544542

545-
MinRequestBodyDataRate = ServerOptions.Limits.MinRequestBodyDataRate;
546543
MinResponseDataRate = ServerOptions.Limits.MinResponseDataRate;
547544
}
548545

src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ internal abstract class Http1MessageBody : MessageBody
1414
protected readonly Http1Connection _context;
1515

1616
protected Http1MessageBody(Http1Connection context)
17-
: base(context, context.MinRequestBodyDataRate)
17+
: base(context)
1818
{
1919
_context = context;
2020
}

src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ private void FastReset()
8484
_currentIHttpRequestLifetimeFeature = this;
8585
_currentIHttpConnectionFeature = this;
8686
_currentIHttpMaxRequestBodySizeFeature = this;
87+
_currentIHttpMinRequestBodyDataRateFeature = this;
8788
_currentIHttpBodyControlFeature = this;
8889
_currentIHttpResponseStartFeature = this;
8990
_currentIRouteValuesFeature = this;
@@ -100,7 +101,6 @@ private void FastReset()
100101
_currentITlsConnectionFeature = null;
101102
_currentIHttpWebSocketFeature = null;
102103
_currentISessionFeature = null;
103-
_currentIHttpMinRequestBodyDataRateFeature = null;
104104
_currentIHttpMinResponseDataRateFeature = null;
105105
_currentIHttpSendFileFeature = null;
106106
}

src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ public HttpProtocol(HttpConnectionContext context)
101101
public string ConnectionIdFeature { get; set; }
102102
public bool HasStartedConsumingRequestBody { get; set; }
103103
public long? MaxRequestBodySize { get; set; }
104+
public MinDataRate MinRequestBodyDataRate { get; set; }
104105
public bool AllowSynchronousIO { get; set; }
105106

106107
/// <summary>
@@ -340,6 +341,7 @@ public void Reset()
340341

341342
HasStartedConsumingRequestBody = false;
342343
MaxRequestBodySize = ServerOptions.Limits.MaxRequestBodySize;
344+
MinRequestBodyDataRate = ServerOptions.Limits.MinRequestBodyDataRate;
343345
AllowSynchronousIO = ServerOptions.AllowSynchronousIO;
344346
TraceIdentifier = null;
345347
Method = HttpMethod.None;

src/Servers/Kestrel/Core/src/Internal/Http/MessageBody.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ internal abstract class MessageBody
1515
private static readonly MessageBody _zeroContentLengthKeepAlive = new ZeroContentLengthMessageBody(keepAlive: true);
1616

1717
private readonly HttpProtocol _context;
18-
private readonly MinDataRate _minRequestBodyDataRate;
1918

2019
private bool _send100Continue = true;
2120
private long _consumedBytes;
@@ -25,10 +24,9 @@ internal abstract class MessageBody
2524
protected bool _backpressure;
2625
protected long _alreadyTimedBytes;
2726

28-
protected MessageBody(HttpProtocol context, MinDataRate minRequestBodyDataRate)
27+
protected MessageBody(HttpProtocol context)
2928
{
3029
_context = context;
31-
_minRequestBodyDataRate = minRequestBodyDataRate;
3230
}
3331

3432
public static MessageBody ZeroContentLengthClose => _zeroContentLengthClose;
@@ -98,10 +96,10 @@ protected void TryStart()
9896
{
9997
Log.RequestBodyStart(_context.ConnectionIdFeature, _context.TraceIdentifier);
10098

101-
if (_minRequestBodyDataRate != null)
99+
if (_context.MinRequestBodyDataRate != null)
102100
{
103101
_timingEnabled = true;
104-
_context.TimeoutControl.StartRequestBody(_minRequestBodyDataRate);
102+
_context.TimeoutControl.StartRequestBody(_context.MinRequestBodyDataRate);
105103
}
106104
}
107105

src/Servers/Kestrel/Core/src/Internal/Http/ZeroContentLengthMessageBody.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
1111
internal sealed class ZeroContentLengthMessageBody : MessageBody
1212
{
1313
public ZeroContentLengthMessageBody(bool keepAlive)
14-
: base(null, null)
14+
: base(null)
1515
{
1616
RequestKeepAlive = keepAlive;
1717
}

src/Servers/Kestrel/Core/src/Internal/Http2/Http2MessageBody.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ internal sealed class Http2MessageBody : MessageBody
1616
private ReadResult _readResult;
1717
private long _alreadyExaminedInNextReadResult;
1818

19-
private Http2MessageBody(Http2Stream context, MinDataRate minRequestBodyDataRate)
20-
: base(context, minRequestBodyDataRate)
19+
private Http2MessageBody(Http2Stream context)
20+
: base(context)
2121
{
2222
_context = context;
2323
}
@@ -47,14 +47,14 @@ protected override void OnDataRead(long bytesRead)
4747
AddAndCheckConsumedBytes(bytesRead);
4848
}
4949

50-
public static MessageBody For(Http2Stream context, MinDataRate minRequestBodyDataRate)
50+
public static MessageBody For(Http2Stream context)
5151
{
5252
if (context.ReceivedEmptyRequestBody)
5353
{
5454
return ZeroContentLengthClose;
5555
}
5656

57-
return new Http2MessageBody(context, minRequestBodyDataRate);
57+
return new Http2MessageBody(context);
5858
}
5959

6060
public override void AdvanceTo(SequencePosition consumed)

src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.FeatureCollection.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
45
using Microsoft.AspNetCore.Http;
56
using Microsoft.AspNetCore.Http.Features;
67
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
78
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
89

910
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
1011
{
11-
internal partial class Http2Stream : IHttp2StreamIdFeature, IHttpResponseTrailersFeature
12+
internal partial class Http2Stream : IHttp2StreamIdFeature,
13+
IHttpMinRequestBodyDataRateFeature,
14+
IHttpResponseTrailersFeature
15+
1216
{
1317
internal HttpResponseTrailers Trailers { get; set; }
1418
private IHeaderDictionary _userTrailers;
@@ -30,5 +34,19 @@ IHeaderDictionary IHttpResponseTrailersFeature.Trailers
3034
}
3135

3236
int IHttp2StreamIdFeature.StreamId => _context.StreamId;
37+
38+
MinDataRate IHttpMinRequestBodyDataRateFeature.MinDataRate
39+
{
40+
get => throw new NotSupportedException(CoreStrings.Http2MinDataRateNotSupported);
41+
set
42+
{
43+
if (value != null)
44+
{
45+
throw new NotSupportedException(CoreStrings.Http2MinDataRateNotSupported);
46+
}
47+
48+
MinRequestBodyDataRate = value;
49+
}
50+
}
3351
}
3452
}

src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ protected override string CreateRequestId()
125125
=> StringUtilities.ConcatAsHexSuffix(ConnectionId, ':', (uint)StreamId);
126126

127127
protected override MessageBody CreateMessageBody()
128-
=> Http2MessageBody.For(this, ServerOptions.Limits.MinRequestBodyDataRate);
128+
=> Http2MessageBody.For(this);
129129

130130
// Compare to Http1Connection.OnStartLine
131131
protected override bool TryParseRequest(ReadResult result, out bool endConnection)

src/Servers/Kestrel/Core/src/Properties/CoreStrings.Designer.cs

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Servers/Kestrel/Core/test/BodyControlTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ public async Task ResponsePipeThrowsObjectDisposedExceptionAfterStop()
145145
private class MockMessageBody : MessageBody
146146
{
147147
public MockMessageBody(bool upgradeable = false)
148-
: base(null, null)
148+
: base(null)
149149
{
150150
RequestUpgrade = upgradeable;
151151
}

src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -888,7 +888,7 @@ public async Task ConsumesRequestWhenApplicationDoesNotConsumeIt()
888888
var buffer = new byte[10];
889889
await context.Response.Body.WriteAsync(buffer, 0, 10);
890890
});
891-
var mockMessageBody = new Mock<MessageBody>(null, null);
891+
var mockMessageBody = new Mock<MessageBody>(null);
892892
_http1Connection.NextMessageBody = mockMessageBody.Object;
893893

894894
var requestProcessingTask = _http1Connection.ProcessRequestsAsync(httpApplication);

src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,9 @@ public void FeaturesSetByGenericSameAsByType()
159159
}
160160

161161
[Fact]
162-
public void Http2StreamFeatureCollectionDoesNotIncludeMinRateFeatures()
162+
public void Http2StreamFeatureCollectionDoesNotIncludeIHttpMinResponseDataRateFeature()
163163
{
164-
Assert.Null(_http2Collection.Get<IHttpMinRequestBodyDataRateFeature>());
165164
Assert.Null(_http2Collection.Get<IHttpMinResponseDataRateFeature>());
166-
167-
Assert.NotNull(_collection.Get<IHttpMinRequestBodyDataRateFeature>());
168165
Assert.NotNull(_collection.Get<IHttpMinResponseDataRateFeature>());
169166
}
170167

@@ -177,6 +174,23 @@ public void Http2StreamFeatureCollectionDoesIncludeUpgradeFeature()
177174
Assert.False(upgradeFeature.IsUpgradableRequest);
178175
}
179176

177+
[Fact]
178+
public void Http2StreamFeatureCollectionDoesIncludeIHttpMinRequestBodyDataRateFeature()
179+
{
180+
var minRateFeature = _http2Collection.Get<IHttpMinRequestBodyDataRateFeature>();
181+
182+
Assert.NotNull(minRateFeature);
183+
184+
Assert.Throws<NotSupportedException>(() => minRateFeature.MinDataRate);
185+
Assert.Throws<NotSupportedException>(() => minRateFeature.MinDataRate = new MinDataRate(1, TimeSpan.FromSeconds(2)));
186+
187+
// You can set the MinDataRate to null though.
188+
minRateFeature.MinDataRate = null;
189+
190+
// But you still cannot read the property;
191+
Assert.Throws<NotSupportedException>(() => minRateFeature.MinDataRate);
192+
}
193+
180194
private void CompareGenericGetterToIndexer()
181195
{
182196
Assert.Same(_collection.Get<IHttpRequestFeature>(), _collection[typeof(IHttpRequestFeature)]);

src/Servers/Kestrel/Core/test/HttpRequestStreamTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ public async Task SynchronousReadsThrowIfDisallowedByIHttpBodyControlFeature()
108108

109109
var mockBodyControl = new Mock<IHttpBodyControlFeature>();
110110
mockBodyControl.Setup(m => m.AllowSynchronousIO).Returns(() => allowSynchronousIO);
111-
var mockMessageBody = new Mock<MessageBody>(null, null);
111+
var mockMessageBody = new Mock<MessageBody>(null);
112112
mockMessageBody.Setup(m => m.ReadAsync(CancellationToken.None)).Returns(new ValueTask<ReadResult>(new ReadResult(default, isCanceled: false, isCompleted: true)));
113113

114114
var pipeReader = new HttpRequestPipeReader();

src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,60 @@ await WaitForConnectionErrorAsync<ConnectionAbortedException>(
798798
_mockConnectionContext.VerifyNoOtherCalls();
799799
}
800800

801+
[Fact]
802+
public async Task DATA_Received_SlowlyWhenRateLimitDisabledPerRequest_DoesNotAbortConnection()
803+
{
804+
var mockSystemClock = _serviceContext.MockSystemClock;
805+
var limits = _serviceContext.ServerOptions.Limits;
806+
807+
// Use non-default value to ensure the min request and response rates aren't mixed up.
808+
limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5));
809+
810+
_timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks);
811+
812+
await InitializeConnectionAsync(context =>
813+
{
814+
// Completely disable rate limiting for this stream.
815+
context.Features.Get<IHttpMinRequestBodyDataRateFeature>().MinDataRate = null;
816+
return _readRateApplication(context);
817+
});
818+
819+
// _helloWorldBytes is 12 bytes, and 12 bytes / 240 bytes/sec = .05 secs which is far below the grace period.
820+
await StartStreamAsync(1, ReadRateRequestHeaders(_helloWorldBytes.Length), endStream: false);
821+
await SendDataAsync(1, _helloWorldBytes, endStream: false);
822+
823+
await ExpectAsync(Http2FrameType.HEADERS,
824+
withLength: 37,
825+
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
826+
withStreamId: 1);
827+
828+
await ExpectAsync(Http2FrameType.DATA,
829+
withLength: 1,
830+
withFlags: (byte)Http2DataFrameFlags.NONE,
831+
withStreamId: 1);
832+
833+
// Don't send any more data and advance just to and then past the grace period.
834+
AdvanceClock(limits.MinRequestBodyDataRate.GracePeriod);
835+
836+
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
837+
838+
AdvanceClock(TimeSpan.FromTicks(1));
839+
840+
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
841+
842+
await SendDataAsync(1, _helloWorldBytes, endStream: true);
843+
844+
await ExpectAsync(Http2FrameType.DATA,
845+
withLength: 0,
846+
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
847+
withStreamId: 1);
848+
849+
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
850+
851+
_mockTimeoutHandler.VerifyNoOtherCalls();
852+
_mockConnectionContext.VerifyNoOtherCalls();
853+
}
854+
801855
[Fact]
802856
public async Task DATA_Received_SlowlyDueToConnectionFlowControl_DoesNotAbortConnection()
803857
{

src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ public static string GenerateFile()
7171
"IHttpRequestLifetimeFeature",
7272
"IHttpConnectionFeature",
7373
"IHttpMaxRequestBodySizeFeature",
74+
"IHttpMinRequestBodyDataRateFeature",
7475
"IHttpBodyControlFeature",
7576
"IHttpResponseStartFeature",
7677
"IRouteValuesFeature",

0 commit comments

Comments
 (0)