Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ private void UpdateHighestOpenedRequestStreamId(long streamId)
public Http3ControlStream? EncoderStream { get; set; }
public Http3ControlStream? DecoderStream { get; set; }
public string ConnectionId => _context.ConnectionId;
public ITimeoutControl TimeoutControl => _context.TimeoutControl;

public void StopProcessingNextRequest()
=> StopProcessingNextRequest(serverInitiated: true);
Expand Down Expand Up @@ -264,7 +265,7 @@ private void UpdateStreamTimeouts(DateTimeOffset now)

if (stream.StreamTimeoutTicks == default)
{
stream.StreamTimeoutTicks = _context.TimeoutControl.GetResponseDrainDeadline(ticks, minDataRate);
stream.StreamTimeoutTicks = TimeoutControl.GetResponseDrainDeadline(ticks, minDataRate);
}

if (stream.StreamTimeoutTicks < ticks)
Expand Down Expand Up @@ -306,6 +307,9 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
// Don't delay on waiting to send outbound control stream settings.
outboundControlStreamTask = ProcessOutboundControlStreamAsync(outboundControlStream);

// Close the connection if we don't receive any request streams
TimeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive);

while (_stoppedAcceptingStreams == 0)
{
var streamContext = await _multiplexedContext.AcceptAsync(_acceptStreamsCts.Token);
Expand Down Expand Up @@ -494,7 +498,7 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
await _streamCompletionAwaitable;
}

_context.TimeoutControl.CancelTimeout();
TimeoutControl.CancelTimeout();
}
catch
{
Expand Down Expand Up @@ -754,6 +758,11 @@ void IHttp3StreamLifetimeHandler.OnStreamCreated(IHttp3Stream stream)
{
if (stream.IsRequestStream)
{
if (_activeRequestCount == 0 && TimeoutControl.TimerReason == TimeoutReason.KeepAlive)
{
TimeoutControl.CancelTimeout();
}

_activeRequestCount++;
}
_streams[stream.StreamId] = stream;
Expand All @@ -767,6 +776,11 @@ void IHttp3StreamLifetimeHandler.OnStreamCompleted(IHttp3Stream stream)
if (stream.IsRequestStream)
{
_activeRequestCount--;

if (_activeRequestCount == 0)
{
TimeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive);
}
}
_streams.Remove(stream.StreamId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public QuicConnectionListener(
var connectionOptions = new QuicServerConnectionOptions
{
ServerAuthenticationOptions = serverAuthenticationOptions,
IdleTimeout = options.IdleTimeout,
IdleTimeout = Timeout.InfiniteTimeSpan, // Kestrel manages connection lifetimes itself so it can send GoAway's.
MaxInboundBidirectionalStreams = options.MaxBidirectionalStreamCount,
MaxInboundUnidirectionalStreams = options.MaxUnidirectionalStreamCount,
DefaultCloseErrorCode = options.DefaultCloseErrorCode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.QuicTransportOptions.DefaultS
Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.QuicTransportOptions.DefaultStreamErrorCode.set -> void
Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.QuicTransportOptions.MaxBidirectionalStreamCount.get -> int
Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.QuicTransportOptions.MaxUnidirectionalStreamCount.get -> int
*REMOVED*Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.QuicTransportOptions.IdleTimeout.get -> System.TimeSpan
*REMOVED*Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.QuicTransportOptions.IdleTimeout.set -> void
*REMOVED*Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.QuicTransportOptions.MaxBidirectionalStreamCount.get -> ushort
*REMOVED*Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.QuicTransportOptions.MaxUnidirectionalStreamCount.get -> ushort
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ public sealed class QuicTransportOptions
/// </summary>
public int MaxUnidirectionalStreamCount { get; set; } = 10;

/// <summary>
/// Sets the idle timeout for connections and streams.
/// </summary>
public TimeSpan IdleTimeout { get; set; } = TimeSpan.FromSeconds(130); // Matches KestrelServerLimits.KeepAliveTimeout.

/// <summary>
/// The maximum read size.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ public static QuicTransportFactory CreateTransportFactory(
long defaultCloseErrorCode = 0)
{
var quicTransportOptions = new QuicTransportOptions();
quicTransportOptions.IdleTimeout = TimeSpan.FromMinutes(1);
quicTransportOptions.MaxBidirectionalStreamCount = 200;
quicTransportOptions.MaxUnidirectionalStreamCount = 200;
quicTransportOptions.DefaultCloseErrorCode = defaultCloseErrorCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,122 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Reflection.PortableExecutable;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
using Xunit.Sdk;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests;

public class Http3TimeoutTests : Http3TestBase
{
[Fact]
public async Task KeepAliveTimeout_ControlStreamNotReceived_ConnectionClosed()
{
var limits = _serviceContext.ServerOptions.Limits;

await Http3Api.InitializeConnectionAsync(_noopApplication).DefaultTimeout();

var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout();
await controlStream.ExpectSettingsAsync().DefaultTimeout();

Http3Api.AdvanceClock(limits.KeepAliveTimeout + TimeSpan.FromTicks(1));

await Http3Api.WaitForConnectionStopAsync(0, false, expectedErrorCode: Http3ErrorCode.NoError);
}

[Fact]
public async Task KeepAliveTimeout_RequestNotReceived_ConnectionClosed()
{
var limits = _serviceContext.ServerOptions.Limits;

await Http3Api.InitializeConnectionAsync(_noopApplication).DefaultTimeout();
await Http3Api.CreateControlStream();

var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout();
await controlStream.ExpectSettingsAsync().DefaultTimeout();

Http3Api.AdvanceClock(limits.KeepAliveTimeout + TimeSpan.FromTicks(1));

await Http3Api.WaitForConnectionStopAsync(0, false, expectedErrorCode: Http3ErrorCode.NoError);
}

[Fact]
public async Task KeepAliveTimeout_AfterRequestComplete_ConnectionClosed()
{
var requestHeaders = new[]
{
new KeyValuePair<string, string>(InternalHeaderNames.Method, "GET"),
new KeyValuePair<string, string>(InternalHeaderNames.Path, "/"),
new KeyValuePair<string, string>(InternalHeaderNames.Scheme, "http"),
};

var limits = _serviceContext.ServerOptions.Limits;

await Http3Api.InitializeConnectionAsync(_noopApplication).DefaultTimeout();

await Http3Api.CreateControlStream();
var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout();
await controlStream.ExpectSettingsAsync().DefaultTimeout();
var requestStream = await Http3Api.CreateRequestStream(requestHeaders, endStream: true);
await requestStream.ExpectHeadersAsync();

await requestStream.ExpectReceiveEndOfStream();
await requestStream.OnDisposedTask.DefaultTimeout();

Http3Api.AdvanceClock(limits.KeepAliveTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1));

await Http3Api.WaitForConnectionStopAsync(4, false, expectedErrorCode: Http3ErrorCode.NoError);
}

[Fact]
public async Task KeepAliveTimeout_LongRunningRequest_KeepsConnectionAlive()
{
var requestHeaders = new[]
{
new KeyValuePair<string, string>(InternalHeaderNames.Method, "GET"),
new KeyValuePair<string, string>(InternalHeaderNames.Path, "/"),
new KeyValuePair<string, string>(InternalHeaderNames.Scheme, "http"),
};

var limits = _serviceContext.ServerOptions.Limits;
var requestReceivedTcs = new TaskCompletionSource();
var requestFinishedTcs = new TaskCompletionSource();

await Http3Api.InitializeConnectionAsync(_ =>
{
requestReceivedTcs.SetResult();
return requestFinishedTcs.Task;
}).DefaultTimeout();

await Http3Api.CreateControlStream();
var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout();
await controlStream.ExpectSettingsAsync().DefaultTimeout();
var requestStream = await Http3Api.CreateRequestStream(requestHeaders, endStream: true);

await requestReceivedTcs.Task;

Http3Api.AdvanceClock(limits.KeepAliveTimeout);
Http3Api.AdvanceClock(limits.KeepAliveTimeout);
Http3Api.AdvanceClock(limits.KeepAliveTimeout);
Http3Api.AdvanceClock(limits.KeepAliveTimeout);
Http3Api.AdvanceClock(limits.KeepAliveTimeout);

requestFinishedTcs.SetResult();

await requestStream.ExpectHeadersAsync();

await requestStream.ExpectReceiveEndOfStream();
await requestStream.OnDisposedTask.DefaultTimeout();

Http3Api.AdvanceClock(limits.KeepAliveTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1));

await Http3Api.WaitForConnectionStopAsync(4, false, expectedErrorCode: Http3ErrorCode.NoError);
}

[Fact]
public async Task HEADERS_IncompleteFrameReceivedWithinRequestHeadersTimeout_StreamError()
Expand Down