Skip to content

Commit 32cdb30

Browse files
committed
HTTP/3 RequestHeadersTimeout
1 parent a4bc912 commit 32cdb30

18 files changed

+509
-171
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,4 +683,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
683683
<data name="Http3StreamErrorRequestEndedNoHeaders" xml:space="preserve">
684684
<value>Request stream ended without headers.</value>
685685
</data>
686+
<data name="Http3ControlStreamHeaderTimeout" xml:space="preserve">
687+
<value>Reading the control stream header timed out.</value>
688+
</data>
686689
</root>

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

Lines changed: 110 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Concurrent;
56
using System.Collections.Generic;
67
using System.Diagnostics;
78
using System.IO;
@@ -14,14 +15,16 @@
1415
using Microsoft.AspNetCore.Connections.Features;
1516
using Microsoft.AspNetCore.Hosting.Server;
1617
using Microsoft.AspNetCore.Http.Features;
18+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
1719
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
1820
using Microsoft.Extensions.Logging;
1921

2022
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
2123
{
22-
internal class Http3Connection : ITimeoutHandler
24+
internal class Http3Connection : ITimeoutHandler, IHttp3StreamLifetimeHandler
2325
{
2426
internal readonly Dictionary<long, Http3Stream> _streams = new Dictionary<long, Http3Stream>();
27+
internal readonly ConcurrentQueue<IHttp3Stream> _startingStreams = new ConcurrentQueue<IHttp3Stream>();
2528

2629
private long _highestOpenedStreamId;
2730
private readonly object _sync = new object();
@@ -84,9 +87,6 @@ public async Task ProcessStreamsAsync<TContext>(IHttpApplication<TContext> httpA
8487
{
8588
try
8689
{
87-
// Ensure TimeoutControl._lastTimestamp is initialized before anything that could set timeouts runs.
88-
_timeoutControl.Initialize(_systemClock.UtcNowTicks);
89-
9090
var connectionHeartbeatFeature = _context.ConnectionFeatures.Get<IConnectionHeartbeatFeature>();
9191
var connectionLifetimeNotificationFeature = _context.ConnectionFeatures.Get<IConnectionLifetimeNotificationFeature>();
9292

@@ -200,11 +200,61 @@ public void Tick()
200200

201201
// It's safe to use UtcNowUnsynchronized since Tick is called by the Heartbeat.
202202
var now = _systemClock.UtcNowUnsynchronized;
203-
_timeoutControl.Tick(now);
203+
204+
UpdateStartingStreams(now.Ticks);
204205

205206
// TODO cancel process stream loop to update logic.
206207
}
207208

209+
private void UpdateStartingStreams(long now)
210+
{
211+
IHttp3Stream? firstRequedStream = null;
212+
213+
while (_startingStreams.TryDequeue(out var stream))
214+
{
215+
if (stream == firstRequedStream)
216+
{
217+
// We've checked every stream that was in _startingStreams.
218+
_startingStreams.Enqueue(stream);
219+
break;
220+
}
221+
222+
if (stream.HasStarted)
223+
{
224+
continue;
225+
}
226+
227+
if (stream.StartExpirationTicks == default)
228+
{
229+
stream.StartExpirationTicks = now + _context.ServiceContext.ServerOptions.Limits.RequestHeadersTimeout.Ticks >= 0
230+
? now + _context.ServiceContext.ServerOptions.Limits.RequestHeadersTimeout.Ticks
231+
: long.MaxValue;
232+
}
233+
234+
if (stream.StartExpirationTicks < now)
235+
{
236+
if (stream.IsRequestStream)
237+
{
238+
stream.Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestHeadersTimeout), Http3ErrorCode.RequestRejected);
239+
}
240+
else
241+
{
242+
stream.Abort(new ConnectionAbortedException(CoreStrings.Http3ControlStreamHeaderTimeout), Http3ErrorCode.StreamCreationError);
243+
}
244+
}
245+
else
246+
{
247+
if (firstRequedStream == null)
248+
{
249+
firstRequedStream = stream;
250+
}
251+
252+
_startingStreams.Enqueue(stream);
253+
}
254+
}
255+
}
256+
257+
208258
public void OnTimeout(TimeoutReason reason)
209259
{
210260
// In the cases that don't log directly here, we expect the setter of the timeout to also be the input
@@ -213,13 +263,11 @@ public void OnTimeout(TimeoutReason reason)
213263
// TODO what timeouts should we handle here? Is keep alive something we should care about?
214264
switch (reason)
215265
{
216-
case TimeoutReason.KeepAlive:
217-
SendGoAway(GetHighestStreamId()).Preserve();
218-
break;
219266
case TimeoutReason.TimeoutFeature:
220267
SendGoAway(GetHighestStreamId()).Preserve();
221268
break;
222-
case TimeoutReason.RequestHeaders:
269+
case TimeoutReason.RequestHeaders: // Request header timeout is handled in starting stream queue
270+
case TimeoutReason.KeepAlive: // Keep-alive is handled by msquic
223271
case TimeoutReason.ReadDataRate:
224272
case TimeoutReason.WriteDataRate:
225273
case TimeoutReason.RequestBodyDrain:
@@ -245,8 +293,6 @@ internal async Task InnerProcessStreamsAsync<TContext>(IHttpApplication<TContext
245293
// TODO should we await the control stream task?
246294
var controlTask = CreateControlStream(application);
247295

248-
_timeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive);
249-
250296
try
251297
{
252298
while (_isClosed == 0)
@@ -277,29 +323,32 @@ internal async Task InnerProcessStreamsAsync<TContext>(IHttpApplication<TContext
277323
streamContext.LocalEndPoint as IPEndPoint,
278324
streamContext.RemoteEndPoint as IPEndPoint,
279325
streamContext.Transport,
326+
this,
280327
streamContext,
281328
_serverSettings);
282-
httpConnectionContext.TimeoutControl = _context.TimeoutControl;
329+
httpConnectionContext.TimeoutControl = _timeoutControl;
283330

284331
if (!quicStreamFeature.CanWrite)
285332
{
286333
// Unidirectional stream
287-
var stream = new Http3ControlStream<TContext>(application, this, httpConnectionContext);
334+
var stream = new Http3ControlStream<TContext>(application, httpConnectionContext);
288335
ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
289336
}
290337
else
291338
{
339+
// Request stream
292340
var streamId = streamIdFeature.StreamId;
293341

294342
UpdateHighestStreamId(streamId);
295343

296-
var http3Stream = new Http3Stream<TContext>(application, this, httpConnectionContext);
344+
var http3Stream = new Http3Stream<TContext>(application, httpConnectionContext);
297345
var stream = http3Stream;
298346
lock (_streams)
299347
{
300348
_activeRequestCount++;
301349
_streams[streamId] = http3Stream;
302350
}
351+
303352
KestrelEventSource.Log.RequestQueuedStart(stream, AspNetCore.Http.HttpProtocol.Http3);
304353
ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
305354
}
@@ -363,8 +412,6 @@ internal async Task InnerProcessStreamsAsync<TContext>(IHttpApplication<TContext
363412
{
364413
await _streamCompletionAwaitable;
365414
}
366-
367-
_timeoutControl.CancelTimeout();
368415
}
369416
catch
370417
{
@@ -410,17 +457,6 @@ private void UpdateConnectionState()
410457
SendGoAway(GetHighestStreamId()).Preserve();
411458
}
412459
}
413-
else
414-
{
415-
// TODO should keep-alive timeout be a thing for HTTP/3? MsQuic currently tracks this for us?
416-
if (_timeoutControl.TimerReason == TimeoutReason.None)
417-
{
418-
_timeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive);
419-
}
420-
421-
// Only reason should be keep-alive.
422-
Debug.Assert(_timeoutControl.TimerReason == TimeoutReason.KeepAlive);
423-
}
424460
}
425461
}
426462

@@ -450,11 +486,12 @@ private async ValueTask<Http3ControlStream> CreateNewUnidirectionalStreamAsync<T
450486
streamContext.LocalEndPoint as IPEndPoint,
451487
streamContext.RemoteEndPoint as IPEndPoint,
452488
streamContext.Transport,
489+
this,
453490
streamContext,
454491
_serverSettings);
455-
httpConnectionContext.TimeoutControl = _context.TimeoutControl;
492+
httpConnectionContext.TimeoutControl = _timeoutControl;
456493

457-
return new Http3ControlStream<TContext>(application, this, httpConnectionContext);
494+
return new Http3ControlStream<TContext>(application, httpConnectionContext);
458495
}
459496

460497
private ValueTask<FlushResult> SendGoAway(long id)
@@ -469,30 +506,7 @@ private ValueTask<FlushResult> SendGoAway(long id)
469506
return new ValueTask<FlushResult>();
470507
}
471508

472-
public void ApplyMaxHeaderListSize(long value)
473-
{
474-
}
475-
476-
internal void ApplyBlockedStream(long value)
477-
{
478-
}
479-
480-
internal void ApplyMaxTableCapacity(long value)
481-
{
482-
}
483-
484-
internal void RemoveStream(long streamId)
485-
{
486-
lock (_streams)
487-
{
488-
_activeRequestCount--;
489-
_streams.Remove(streamId);
490-
}
491-
492-
_streamCompletionAwaitable.Complete();
493-
}
494-
495-
public bool SetInboundControlStream(Http3ControlStream stream)
509+
public bool OnInboundControlStream(Http3ControlStream stream)
496510
{
497511
lock (_sync)
498512
{
@@ -505,7 +519,7 @@ public bool SetInboundControlStream(Http3ControlStream stream)
505519
}
506520
}
507521

508-
public bool SetInboundEncoderStream(Http3ControlStream stream)
522+
public bool OnInboundEncoderStream(Http3ControlStream stream)
509523
{
510524
lock (_sync)
511525
{
@@ -518,7 +532,7 @@ public bool SetInboundEncoderStream(Http3ControlStream stream)
518532
}
519533
}
520534

521-
public bool SetInboundDecoderStream(Http3ControlStream stream)
535+
public bool OnInboundDecoderStream(Http3ControlStream stream)
522536
{
523537
lock (_sync)
524538
{
@@ -531,6 +545,47 @@ public bool SetInboundDecoderStream(Http3ControlStream stream)
531545
}
532546
}
533547

548+
public void OnStreamCompleted(IHttp3Stream stream)
549+
{
550+
lock (_streams)
551+
{
552+
_activeRequestCount--;
553+
_streams.Remove(stream.StreamId);
554+
}
555+
556+
_streamCompletionAwaitable.Complete();
557+
}
558+
559+
public void OnStreamConnectionError(Http3ConnectionErrorException ex)
560+
{
561+
Log.Http3ConnectionError(ConnectionId, ex);
562+
Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode);
563+
}
564+
565+
public void OnInboundControlStreamSetting(Http3SettingType type, long value)
566+
{
567+
switch (type)
568+
{
569+
case Http3SettingType.QPackMaxTableCapacity:
570+
break;
571+
case Http3SettingType.MaxFieldSectionSize:
572+
break;
573+
case Http3SettingType.QPackBlockedStreams:
574+
break;
575+
default:
576+
throw new InvalidOperationException("Unexpected setting: " + type);
577+
}
578+
}
579+
580+
public void OnStreamStarting(IHttp3Stream stream)
581+
{
582+
_startingStreams.Enqueue(stream);
583+
}
584+
585+
public void OnStreamStarted(IHttp3Stream stream)
586+
{
587+
}
588+
534589
private static class GracefulCloseInitiator
535590
{
536591
public const int None = 0;

0 commit comments

Comments
 (0)