Skip to content

Commit e554803

Browse files
authored
HTTP/3: Request and response data rates (#32127)
1 parent 51709ac commit e554803

File tree

6 files changed

+524
-12
lines changed

6 files changed

+524
-12
lines changed

src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs

+15-2
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ public void StopProcessingNextRequest(bool serverInitiated)
133133

134134
if (Interlocked.CompareExchange(ref _gracefulCloseInitiator, initiator, GracefulCloseInitiator.None) == GracefulCloseInitiator.None)
135135
{
136+
// https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-5.2-11
137+
// An endpoint that completes a graceful shutdown SHOULD use the H3_NO_ERROR error code
138+
// when closing the connection.
139+
_errorCodeFeature.Error = (long)Http3ErrorCode.NoError;
140+
136141
// Abort accept async loop to initiate graceful shutdown
137142
// TODO aborting connection isn't graceful due to runtime issue, will drop data on streams
138143
// Either we need to swap to using a cts here or fix runtime to gracefully close connection.
@@ -249,10 +254,15 @@ public void OnTimeout(TimeoutReason reason)
249254
case TimeoutReason.TimeoutFeature:
250255
SendGoAway(GetHighestStreamId()).Preserve();
251256
break;
252-
case TimeoutReason.RequestHeaders: // Request header timeout is handled in starting stream queue
253-
case TimeoutReason.KeepAlive: // Keep-alive is handled by msquic
254257
case TimeoutReason.ReadDataRate:
258+
Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestBodyTimeout), Http3ErrorCode.InternalError);
259+
break;
255260
case TimeoutReason.WriteDataRate:
261+
Log.ResponseMinimumDataRateNotSatisfied(_context.ConnectionId, traceIdentifier: null);
262+
Abort(new ConnectionAbortedException(CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied), Http3ErrorCode.InternalError);
263+
break;
264+
case TimeoutReason.RequestHeaders: // Request header timeout is handled in starting stream queue
265+
case TimeoutReason.KeepAlive: // Keep-alive is handled by msquic
256266
case TimeoutReason.RequestBodyDrain:
257267
default:
258268
Debug.Assert(false, "Invalid TimeoutReason");
@@ -392,6 +402,9 @@ internal async Task InnerProcessStreamsAsync<TContext>(IHttpApplication<TContext
392402
{
393403
await _streamCompletionAwaitable;
394404
}
405+
406+
_timeoutControl.CancelTimeout();
407+
_timeoutControl.StartDrainTimeout(Limits.MinResponseDataRate, Limits.MaxResponseBufferSize);
395408
}
396409
catch
397410
{

src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Buffers;
66
using System.Collections.Generic;
7+
using System.Diagnostics;
78
using System.Globalization;
89
using System.IO.Pipelines;
910
using System.Net.Http;

src/Servers/Kestrel/Core/src/Internal/Infrastructure/TimeoutControl.cs

+10
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,19 @@ private void CheckForReadDataRateTimeout(long timestamp)
8181
return;
8282
}
8383

84+
// HTTP/2
8485
// Don't enforce the rate timeout if there is back pressure due to HTTP/2 connection-level input
8586
// flow control. We don't consider stream-level flow control, because we wouldn't be timing a read
8687
// for any stream that didn't have a completely empty stream-level flow control window.
88+
//
89+
// HTTP/3
90+
// This isn't (currently) checked. Reasons:
91+
// - We're not sure how often people in the real-world run into this. If it
92+
// becomes a problem then we'll need to revisit.
93+
// - There isn't a way to get this information easily and efficently from msquic.
94+
// - With QUIC, bytes can be received out of order. The connection window could
95+
// be filled up out of order so that availablility is low but there is still
96+
// no data available to use. Would need a smarter way to handle this situation.
8797
if (_connectionInputFlowControl?.IsAvailabilityLow == true)
8898
{
8999
return;

src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs

+9-2
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,19 @@ await InitializeConnectionAsync(async context =>
6161
}
6262

6363
[Fact]
64-
public async Task GracefulServerShutdownSendsGoawayClosesConnection()
64+
public async Task GracefulServerShutdownClosesConnection()
6565
{
6666
await InitializeConnectionAsync(_echoApplication);
67+
68+
var inboundControlStream = await GetInboundControlStream();
69+
await inboundControlStream.ExpectSettingsAsync();
70+
6771
// Trigger server shutdown.
68-
MultiplexedConnectionContext.ConnectionClosingCts.Cancel();
72+
CloseConnectionGracefully();
73+
6974
Assert.Null(await MultiplexedConnectionContext.AcceptAsync().DefaultTimeout());
75+
76+
await WaitForConnectionStopAsync(0, false, expectedErrorCode: Http3ErrorCode.NoError);
7077
}
7178

7279
[Theory]

src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs

+86-8
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
using System.Collections.Concurrent;
77
using System.Collections.Generic;
88
using System.Diagnostics;
9+
using System.Globalization;
910
using System.IO;
1011
using System.IO.Pipelines;
1112
using System.Linq;
1213
using System.Net.Http;
1314
using System.Net.Http.QPack;
1415
using System.Reflection;
16+
using System.Text;
1517
using System.Threading;
1618
using System.Threading.Channels;
1719
using System.Threading.Tasks;
@@ -38,6 +40,8 @@ public abstract class Http3TestBase : TestApplicationErrorLoggerLoggedTest, IDis
3840
{
3941
protected static readonly int MaxRequestHeaderFieldSize = 16 * 1024;
4042
protected static readonly string _4kHeaderValue = new string('a', 4096);
43+
protected static readonly byte[] _helloWorldBytes = Encoding.ASCII.GetBytes("hello, world");
44+
protected static readonly byte[] _maxData = Encoding.ASCII.GetBytes(new string('a', 16 * 1024));
4145

4246
internal TestServiceContext _serviceContext;
4347
internal readonly TimeoutControl _timeoutControl;
@@ -49,16 +53,37 @@ public abstract class Http3TestBase : TestApplicationErrorLoggerLoggedTest, IDis
4953
protected readonly TaskCompletionSource _closedStateReached = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
5054

5155
internal readonly ConcurrentDictionary<long, Http3StreamBase> _runningStreams = new ConcurrentDictionary<long, Http3StreamBase>();
52-
5356
protected readonly RequestDelegate _noopApplication;
5457
protected readonly RequestDelegate _echoApplication;
58+
protected readonly RequestDelegate _readRateApplication;
5559
protected readonly RequestDelegate _echoMethod;
5660
protected readonly RequestDelegate _echoPath;
5761
protected readonly RequestDelegate _echoHost;
5862

5963
private Http3ControlStream _inboundControlStream;
6064
private long _currentStreamId;
6165

66+
protected static readonly IEnumerable<KeyValuePair<string, string>> _browserRequestHeaders = new[]
67+
{
68+
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
69+
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
70+
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
71+
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
72+
new KeyValuePair<string, string>("user-agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0"),
73+
new KeyValuePair<string, string>("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"),
74+
new KeyValuePair<string, string>("accept-language", "en-US,en;q=0.5"),
75+
new KeyValuePair<string, string>("accept-encoding", "gzip, deflate, br"),
76+
new KeyValuePair<string, string>("upgrade-insecure-requests", "1"),
77+
};
78+
79+
protected static IEnumerable<KeyValuePair<string, string>> ReadRateRequestHeaders(int expectedBytes) => new[]
80+
{
81+
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
82+
new KeyValuePair<string, string>(HeaderNames.Path, "/" + expectedBytes),
83+
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
84+
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
85+
};
86+
6287
public Http3TestBase()
6388
{
6489
_timeoutControl = new TimeoutControl(_mockTimeoutHandler.Object);
@@ -83,6 +108,26 @@ public Http3TestBase()
83108
}
84109
};
85110

111+
_readRateApplication = async context =>
112+
{
113+
var expectedBytes = int.Parse(context.Request.Path.Value.Substring(1), CultureInfo.InvariantCulture);
114+
115+
var buffer = new byte[16 * 1024];
116+
var received = 0;
117+
118+
while (received < expectedBytes)
119+
{
120+
received += await context.Request.Body.ReadAsync(buffer, 0, buffer.Length);
121+
}
122+
123+
var stalledReadTask = context.Request.Body.ReadAsync(buffer, 0, buffer.Length);
124+
125+
// Write to the response so the test knows the app started the stalled read.
126+
await context.Response.Body.WriteAsync(new byte[1], 0, 1);
127+
128+
await stalledReadTask;
129+
};
130+
86131
_echoMethod = context =>
87132
{
88133
context.Response.Headers["Method"] = context.Request.Method;
@@ -151,7 +196,17 @@ internal async ValueTask<Http3ControlStream> GetInboundControlStream()
151196
return _inboundControlStream;
152197
}
153198

154-
internal async Task WaitForConnectionErrorAsync<TException>(bool ignoreNonGoAwayFrames, long expectedLastStreamId, Http3ErrorCode expectedErrorCode, params string[] expectedErrorMessage)
199+
internal void CloseConnectionGracefully()
200+
{
201+
MultiplexedConnectionContext.ConnectionClosingCts.Cancel();
202+
}
203+
204+
internal Task WaitForConnectionStopAsync(long expectedLastStreamId, bool ignoreNonGoAwayFrames, Http3ErrorCode? expectedErrorCode = null)
205+
{
206+
return WaitForConnectionErrorAsync<Exception>(ignoreNonGoAwayFrames, expectedLastStreamId, expectedErrorCode: expectedErrorCode ?? 0, expectedErrorMessage: null);
207+
}
208+
209+
internal async Task WaitForConnectionErrorAsync<TException>(bool ignoreNonGoAwayFrames, long? expectedLastStreamId, Http3ErrorCode expectedErrorCode, params string[] expectedErrorMessage)
155210
where TException : Exception
156211
{
157212
var frame = await _inboundControlStream.ReceiveFrameAsync();
@@ -164,7 +219,10 @@ internal async Task WaitForConnectionErrorAsync<TException>(bool ignoreNonGoAway
164219
}
165220
}
166221

167-
VerifyGoAway(frame, expectedLastStreamId);
222+
if (expectedLastStreamId != null)
223+
{
224+
VerifyGoAway(frame, expectedLastStreamId.GetValueOrDefault());
225+
}
168226

169227
Assert.Equal((Http3ErrorCode)expectedErrorCode, (Http3ErrorCode)MultiplexedConnectionContext.Error);
170228

@@ -216,8 +274,6 @@ protected async Task InitializeConnectionAsync(RequestDelegate application)
216274
_connectionTask = Connection.ProcessStreamsAsync(new DummyApplication(application));
217275

218276
await GetInboundControlStream();
219-
220-
await Task.CompletedTask;
221277
}
222278

223279
internal async ValueTask<Http3RequestStream> InitializeConnectionAndStreamsAsync(RequestDelegate application)
@@ -394,7 +450,7 @@ public class Http3StreamBase : IProtocolErrorCodeFeature
394450
internal DuplexPipe.DuplexPipePair _pair;
395451
internal Http3TestBase _testBase;
396452
internal Http3Connection _connection;
397-
private long _bytesReceived;
453+
public long BytesReceived { get; private set; }
398454
public long Error { get; set; }
399455

400456
public Task OnStreamCreatedTask => _onStreamCreatedTcs.Task;
@@ -412,6 +468,12 @@ protected static async Task FlushAsync(PipeWriter writableBuffer)
412468
await writableBuffer.FlushAsync().AsTask().DefaultTimeout();
413469
}
414470

471+
internal async Task ReceiveEndAsync()
472+
{
473+
var result = await _pair.Application.Input.ReadAsync().AsTask().DefaultTimeout();
474+
Assert.True(result.IsCompleted);
475+
}
476+
415477
internal async Task<Http3FrameWithPayload> ReceiveFrameAsync()
416478
{
417479
var frame = new Http3FrameWithPayload();
@@ -446,7 +508,7 @@ internal async Task<Http3FrameWithPayload> ReceiveFrameAsync()
446508
}
447509
finally
448510
{
449-
_bytesReceived += copyBuffer.Slice(copyBuffer.Start, consumed).Length;
511+
BytesReceived += copyBuffer.Slice(copyBuffer.Start, consumed).Length;
450512
_pair.Application.Input.AdvanceTo(consumed, examined);
451513
}
452514
}
@@ -655,6 +717,17 @@ void WriteSpan(PipeWriter pw)
655717
await FlushAsync(writableBuffer);
656718
}
657719

720+
internal async Task SendGoAwayAsync(long streamId, bool endStream = false)
721+
{
722+
var frame = new Http3RawFrame();
723+
frame.PrepareGoAway();
724+
725+
var data = new byte[VariableLengthIntegerHelper.GetByteCount(streamId)];
726+
VariableLengthIntegerHelper.WriteInteger(data, streamId);
727+
728+
await SendFrameAsync(frame, data, endStream);
729+
}
730+
658731
internal async Task SendSettingsAsync(List<Http3PeerSetting> settings, bool endStream = false)
659732
{
660733
var frame = new Http3RawFrame();
@@ -738,6 +811,7 @@ public class TestMultiplexedConnectionContext : MultiplexedConnectionContext, IC
738811
});
739812

740813
private readonly Http3TestBase _testBase;
814+
private long _error;
741815

742816
public TestMultiplexedConnectionContext(Http3TestBase testBase)
743817
{
@@ -759,7 +833,11 @@ public TestMultiplexedConnectionContext(Http3TestBase testBase)
759833

760834
public CancellationTokenSource ConnectionClosingCts { get; set; } = new CancellationTokenSource();
761835

762-
public long Error { get; set; }
836+
public long Error
837+
{
838+
get => _error;
839+
set => _error = value;
840+
}
763841

764842
public override void Abort()
765843
{

0 commit comments

Comments
 (0)