Skip to content

Commit e35b224

Browse files
committed
HTTP/3 RequestHeadersTimeout
1 parent 75555f0 commit e35b224

19 files changed

+553
-190
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: 92 additions & 62 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,15 @@
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
{
24-
internal readonly Dictionary<long, Http3Stream> _streams = new Dictionary<long, Http3Stream>();
26+
internal readonly Dictionary<long, IHttp3Stream> _streams = new Dictionary<long, IHttp3Stream>();
2527

2628
private long _highestOpenedStreamId;
2729
private readonly object _sync = new object();
@@ -84,9 +86,6 @@ public async Task ProcessStreamsAsync<TContext>(IHttpApplication<TContext> httpA
8486
{
8587
try
8688
{
87-
// Ensure TimeoutControl._lastTimestamp is initialized before anything that could set timeouts runs.
88-
_timeoutControl.Initialize(_systemClock.UtcNowTicks);
89-
9089
var connectionHeartbeatFeature = _context.ConnectionFeatures.Get<IConnectionHeartbeatFeature>();
9190
var connectionLifetimeNotificationFeature = _context.ConnectionFeatures.Get<IConnectionLifetimeNotificationFeature>();
9291

@@ -198,11 +197,42 @@ public void Tick()
198197
return;
199198
}
200199

201-
// It's safe to use UtcNowUnsynchronized since Tick is called by the Heartbeat.
202-
var now = _systemClock.UtcNowUnsynchronized;
203-
_timeoutControl.Tick(now);
200+
UpdateStartingStreams();
201+
}
202+
203+
private void UpdateStartingStreams()
204+
{
205+
var now = _systemClock.UtcNow.Ticks;
206+
207+
lock (_streams)
208+
{
209+
foreach (var stream in _streams.Values)
210+
{
211+
if (stream.ReceivedHeader)
212+
{
213+
continue;
214+
}
215+
216+
if (stream.HeaderTimeoutTicks == default)
217+
{
218+
// On expiration overflow, use max value.
219+
var expirationTicks = now + _context.ServiceContext.ServerOptions.Limits.RequestHeadersTimeout.Ticks;
220+
stream.HeaderTimeoutTicks = expirationTicks >= 0 ? expirationTicks : long.MaxValue;
221+
}
204222

205-
// TODO cancel process stream loop to update logic.
223+
if (stream.HeaderTimeoutTicks < now)
224+
{
225+
if (stream.IsRequestStream)
226+
{
227+
stream.Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestHeadersTimeout), Http3ErrorCode.RequestRejected);
228+
}
229+
else
230+
{
231+
stream.Abort(new ConnectionAbortedException(CoreStrings.Http3ControlStreamHeaderTimeout), Http3ErrorCode.StreamCreationError);
232+
}
233+
}
234+
}
235+
}
206236
}
207237

208238
public void OnTimeout(TimeoutReason reason)
@@ -213,13 +243,11 @@ public void OnTimeout(TimeoutReason reason)
213243
// TODO what timeouts should we handle here? Is keep alive something we should care about?
214244
switch (reason)
215245
{
216-
case TimeoutReason.KeepAlive:
217-
SendGoAway(GetHighestStreamId()).Preserve();
218-
break;
219246
case TimeoutReason.TimeoutFeature:
220247
SendGoAway(GetHighestStreamId()).Preserve();
221248
break;
222-
case TimeoutReason.RequestHeaders:
249+
case TimeoutReason.RequestHeaders: // Request header timeout is handled in starting stream queue
250+
case TimeoutReason.KeepAlive: // Keep-alive is handled by msquic
223251
case TimeoutReason.ReadDataRate:
224252
case TimeoutReason.WriteDataRate:
225253
case TimeoutReason.RequestBodyDrain:
@@ -245,8 +273,6 @@ internal async Task InnerProcessStreamsAsync<TContext>(IHttpApplication<TContext
245273
// TODO should we await the control stream task?
246274
var controlTask = CreateControlStream(application);
247275

248-
_timeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive);
249-
250276
try
251277
{
252278
while (_isClosed == 0)
@@ -277,29 +303,36 @@ internal async Task InnerProcessStreamsAsync<TContext>(IHttpApplication<TContext
277303
streamContext.LocalEndPoint as IPEndPoint,
278304
streamContext.RemoteEndPoint as IPEndPoint,
279305
streamContext.Transport,
306+
this,
280307
streamContext,
281308
_serverSettings);
282309
httpConnectionContext.TimeoutControl = _context.TimeoutControl;
283310

311+
var streamId = streamIdFeature.StreamId;
312+
284313
if (!quicStreamFeature.CanWrite)
285314
{
286315
// Unidirectional stream
287-
var stream = new Http3ControlStream<TContext>(application, this, httpConnectionContext);
316+
var stream = new Http3ControlStream<TContext>(application, httpConnectionContext);
317+
lock (_streams)
318+
{
319+
_streams[streamId] = stream;
320+
}
321+
288322
ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
289323
}
290324
else
291325
{
292-
var streamId = streamIdFeature.StreamId;
293-
326+
// Request stream
294327
UpdateHighestStreamId(streamId);
295328

296-
var http3Stream = new Http3Stream<TContext>(application, this, httpConnectionContext);
297-
var stream = http3Stream;
329+
var stream = new Http3Stream<TContext>(application, httpConnectionContext);
298330
lock (_streams)
299331
{
300332
_activeRequestCount++;
301-
_streams[streamId] = http3Stream;
333+
_streams[streamId] = stream;
302334
}
335+
303336
KestrelEventSource.Log.RequestQueuedStart(stream, AspNetCore.Http.HttpProtocol.Http3);
304337
ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
305338
}
@@ -363,8 +396,6 @@ internal async Task InnerProcessStreamsAsync<TContext>(IHttpApplication<TContext
363396
{
364397
await _streamCompletionAwaitable;
365398
}
366-
367-
_timeoutControl.CancelTimeout();
368399
}
369400
catch
370401
{
@@ -410,17 +441,6 @@ private void UpdateConnectionState()
410441
SendGoAway(GetHighestStreamId()).Preserve();
411442
}
412443
}
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-
}
424444
}
425445
}
426446

@@ -450,11 +470,12 @@ private async ValueTask<Http3ControlStream> CreateNewUnidirectionalStreamAsync<T
450470
streamContext.LocalEndPoint as IPEndPoint,
451471
streamContext.RemoteEndPoint as IPEndPoint,
452472
streamContext.Transport,
473+
this,
453474
streamContext,
454475
_serverSettings);
455476
httpConnectionContext.TimeoutControl = _context.TimeoutControl;
456477

457-
return new Http3ControlStream<TContext>(application, this, httpConnectionContext);
478+
return new Http3ControlStream<TContext>(application, httpConnectionContext);
458479
}
459480

460481
private ValueTask<FlushResult> SendGoAway(long id)
@@ -466,33 +487,10 @@ private ValueTask<FlushResult> SendGoAway(long id)
466487
return OutboundControlStream.SendGoAway(id);
467488
}
468489
}
469-
return new ValueTask<FlushResult>();
490+
return default;
470491
}
471492

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)
493+
public bool OnInboundControlStream(Http3ControlStream stream)
496494
{
497495
lock (_sync)
498496
{
@@ -505,7 +503,7 @@ public bool SetInboundControlStream(Http3ControlStream stream)
505503
}
506504
}
507505

508-
public bool SetInboundEncoderStream(Http3ControlStream stream)
506+
public bool OnInboundEncoderStream(Http3ControlStream stream)
509507
{
510508
lock (_sync)
511509
{
@@ -518,7 +516,7 @@ public bool SetInboundEncoderStream(Http3ControlStream stream)
518516
}
519517
}
520518

521-
public bool SetInboundDecoderStream(Http3ControlStream stream)
519+
public bool OnInboundDecoderStream(Http3ControlStream stream)
522520
{
523521
lock (_sync)
524522
{
@@ -531,6 +529,38 @@ public bool SetInboundDecoderStream(Http3ControlStream stream)
531529
}
532530
}
533531

532+
public void OnStreamCompleted(IHttp3Stream stream)
533+
{
534+
lock (_streams)
535+
{
536+
_activeRequestCount--;
537+
_streams.Remove(stream.StreamId);
538+
}
539+
540+
_streamCompletionAwaitable.Complete();
541+
}
542+
543+
public void OnStreamConnectionError(Http3ConnectionErrorException ex)
544+
{
545+
Log.Http3ConnectionError(ConnectionId, ex);
546+
Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode);
547+
}
548+
549+
public void OnInboundControlStreamSetting(Http3SettingType type, long value)
550+
{
551+
switch (type)
552+
{
553+
case Http3SettingType.QPackMaxTableCapacity:
554+
break;
555+
case Http3SettingType.MaxFieldSectionSize:
556+
break;
557+
case Http3SettingType.QPackBlockedStreams:
558+
break;
559+
default:
560+
throw new InvalidOperationException("Unexpected setting: " + type);
561+
}
562+
}
563+
534564
private static class GracefulCloseInitiator
535565
{
536566
public const int None = 0;

0 commit comments

Comments
 (0)