/// Central repository for default values used in http handler settings. Not all settings are relevant
/// to or configurable by all handlers.
/// </summary>
- internal static class HttpHandlerDefaults
+ internal static partial class HttpHandlerDefaults
{
public const int DefaultMaxAutomaticRedirections = 50;
public const int DefaultMaxConnectionsPerServer = int.MaxValue;
public class PingFrame : Frame
{
- public byte[] Data;
+ public long Data;
- public PingFrame(byte[] data, FrameFlags flags, int streamId) :
+ public PingFrame(long data, FrameFlags flags, int streamId) :
base(8, FrameType.Ping, flags, streamId)
{
Data = data;
public static PingFrame ReadFrom(Frame header, ReadOnlySpan<byte> buffer)
{
- byte[] data = buffer.ToArray();
+ long data = BinaryPrimitives.ReadInt64BigEndian(buffer);
return new PingFrame(data, header.Flags, header.StreamId);
}
base.WriteTo(buffer);
buffer = buffer.Slice(Frame.FrameHeaderLength, 8);
- Data.CopyTo(buffer);
+ BinaryPrimitives.WriteInt64BigEndian(buffer, Data);
}
public override string ToString()
{
- return base.ToString() + $"\nOpaque Data: {string.Join(", ", Data)}";
+ return base.ToString() + $"\nOpaque Data: {Data:X16}";
}
}
private Stream _connectionStream;
private TaskCompletionSource<bool> _ignoredSettingsAckPromise;
private bool _ignoreWindowUpdates;
+ private TaskCompletionSource<PingFrame> _expectPingFrame;
private readonly TimeSpan _timeout;
private int _lastStreamId;
return await ReadFrameAsync(cancellationToken).ConfigureAwait(false);
}
+ if (_expectPingFrame != null && header.Type == FrameType.Ping)
+ {
+ _expectPingFrame.SetResult(PingFrame.ReadFrom(header, data));
+ _expectPingFrame = null;
+ return await ReadFrameAsync(cancellationToken).ConfigureAwait(false);
+ }
+
// Construct the correct frame type and return it.
switch (header.Type)
{
_ignoreWindowUpdates = true;
}
+ // Set up loopback server to expect PING frames among other frames.
+ // Once PING frame is read in ReadFrameAsync, the returned task is completed.
+ // The returned task is canceled in ReadPingAsync if no PING frame has been read so far.
+ public Task<PingFrame> ExpectPingFrameAsync()
+ {
+ _expectPingFrame ??= new TaskCompletionSource<PingFrame>();
+ return _expectPingFrame.Task;
+ }
+
public async Task ReadRstStreamAsync(int streamId)
{
Frame frame = await ReadFrameAsync(_timeout);
public async Task PingPong()
{
- byte[] pingData = new byte[8] { 1, 2, 3, 4, 50, 60, 70, 80 };
+ long pingData = BitConverter.ToInt64(new byte[8] { 1, 2, 3, 4, 50, 60, 70, 80 }, 0);
PingFrame ping = new PingFrame(pingData, FrameFlags.None, 0);
await WriteFrameAsync(ping).ConfigureAwait(false);
PingFrame pingAck = (PingFrame)await ReadFrameAsync(_timeout).ConfigureAwait(false);
Assert.Equal(pingData, pingAck.Data);
}
+ public async Task<PingFrame> ReadPingAsync(TimeSpan timeout)
+ {
+ _expectPingFrame?.TrySetCanceled();
+ _expectPingFrame = null;
+
+ Frame frame = await ReadFrameAsync(timeout).ConfigureAwait(false);
+ Assert.NotNull(frame);
+ Assert.Equal(FrameType.Ping, frame.Type);
+ Assert.Equal(0, frame.StreamId);
+ Assert.False(frame.AckFlag);
+ Assert.Equal(8, frame.Length);
+
+ return Assert.IsAssignableFrom<PingFrame>(frame);
+ }
+
+ public async Task SendPingAckAsync(long payload)
+ {
+ PingFrame pingAck = new PingFrame(payload, FrameFlags.Ack, 0);
+ await WriteFrameAsync(pingAck).ConfigureAwait(false);
+ }
+
public async Task SendDefaultResponseHeadersAsync(int streamId)
{
byte[] headers = new byte[] { 0x88 }; // Encoding for ":status: 200"
<Compile Include="$(CommonPath)\Interop\Windows\Crypt32\Interop.certificates_types.cs"
Link="Common\Interop\Windows\Crypt32\Interop.certificates_types.cs" />
<Compile Include="$(CommonPath)\Interop\Windows\Crypt32\Interop.certificates.cs"
- Link="Common\Interop\Windows\Crypt32\Interop.certificates.cs" />
+ Link="Common\Interop\Windows\Crypt32\Interop.certificates.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.FormatMessage.cs"
- Link="Common\Interop\Windows\Kernel32\Interop.FormatMessage.cs" />
+ Link="Common\Interop\Windows\Kernel32\Interop.FormatMessage.cs" />
<Compile Include="$(CommonPath)\Interop\Windows\Kernel32\Interop.GetModuleHandle.cs"
Link="Common\Interop\Windows\Kernel32\Interop.GetModuleHandle.cs" />
<Compile Include="$(CommonPath)\Interop\Windows\Interop.HRESULT_FROM_WIN32.cs"
<Compile Include="$(CommonPath)\System\Net\HttpKnownHeaderNames.cs"
Link="Common\System\Net\HttpKnownHeaderNames.cs" />
<Compile Include="$(CommonPath)\System\Net\HttpKnownHeaderNames.TryGetHeaderName.cs"
- Link="Common\System\Net\HttpKnownHeaderNames.TryGetHeaderName.cs" />
+ Link="Common\System\Net\HttpKnownHeaderNames.TryGetHeaderName.cs" />
<Compile Include="$(CommonPath)System\Net\HttpStatusDescription.cs"
Link="Common\System\Net\Http\HttpStatusDescription.cs" />
<Compile Include="$(CommonPath)\System\Net\SecurityProtocol.cs"
<Compile Include="$(CommonPath)\System\Runtime\ExceptionServices\ExceptionStackTrace.cs"
Link="Common\System\Runtime\ExceptionServices\ExceptionStackTrace.cs" />
<Compile Include="$(CommonPath)\System\Threading\Tasks\RendezvousAwaitable.cs"
- Link="Common\System\Threading\Tasks\RendezvousAwaitable.cs" />
+ Link="Common\System\Threading\Tasks\RendezvousAwaitable.cs" />
<Compile Include="$(CommonPath)System\Threading\Tasks\TaskToApm.cs"
Link="Common\System\Threading\Tasks\TaskToApm.cs" />
<Compile Include="System\Net\Http\NetEventSource.WinHttpHandler.cs" />
public System.Net.ICredentials? Credentials { get { throw null; } set { } }
public System.Net.ICredentials? DefaultProxyCredentials { get { throw null; } set { } }
public System.TimeSpan Expect100ContinueTimeout { get { throw null; } set { } }
+ public System.TimeSpan KeepAlivePingDelay { get { throw null; } set { } }
+ public System.TimeSpan KeepAlivePingTimeout { get { throw null; } set { } }
+ public HttpKeepAlivePingPolicy KeepAlivePingPolicy { get { throw null; } set { } }
public int MaxAutomaticRedirections { get { throw null; } set { } }
public int MaxConnectionsPerServer { get { throw null; } set { } }
public int MaxResponseDrainSize { get { throw null; } set { } }
protected internal override System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage> SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { throw null; }
public bool EnableMultipleHttp2Connections { get { throw null; } set { } }
}
+ public enum HttpKeepAlivePingPolicy
+ {
+ WithActiveRequests,
+ Always
+ }
public partial class StreamContent : System.Net.Http.HttpContent
{
public StreamContent(System.IO.Stream content) { }
<?xml version="1.0" encoding="utf-8"?>
<root>
- <!--
- Microsoft ResX Schema
-
+ <!--
+ Microsoft ResX Schema
+
Version 2.0
-
- The primary goals of this format is to allow a simple XML format
- that is mostly human readable. The generation and parsing of the
- various data types are done through the TypeConverter classes
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
associated with the data types.
-
+
Example:
-
+
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
-
- There are any number of "resheader" rows that contain simple
+
+ There are any number of "resheader" rows that contain simple
name/value pairs.
-
- Each data row contains a name, and value. The row also contains a
- type or mimetype. Type corresponds to a .NET class that support
- text/value conversion through the TypeConverter architecture.
- Classes that don't support this are serialized and stored with the
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
mimetype set.
-
- The mimetype is used for serialized objects, and tells the
- ResXResourceReader how to depersist the object. This is currently not
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
-
- Note - application/x-microsoft.net.object.binary.base64 is the format
- that the ResXResourceWriter will generate, however the reader can
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
-
+
mimetype: application/x-microsoft.net.object.binary.base64
- value : The object must be serialized with
+ value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
-
+
mimetype: application/x-microsoft.net.object.soap.base64
- value : The object must be serialized with
+ value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
- value : The object must be serialized into a byte array
+ value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<data name="net_http_value_must_be_greater_than" xml:space="preserve">
<value>The specified value must be greater than {0}.</value>
</data>
+ <data name="net_http_value_must_be_greater_than_or_equal" xml:space="preserve">
+ <value>The specified value '{0}' must be greater than or equal to '{1}'.</value>
+ </data>
<data name="MailHeaderFieldInvalidCharacter" xml:space="preserve">
<value>An invalid character was found in the mail header: '{0}'.</value>
</data>
</ItemGroup>
<!-- SocketsHttpHandler implementation -->
<ItemGroup Condition="'$(TargetsBrowser)' != 'true'">
+ <Compile Include="System\Net\Http\HttpHandlerDefaults.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\AuthenticationHelper.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\AuthenticationHelper.Digest.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\AuthenticationHelper.NtAuth.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpContentReadStream.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpContentStream.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpContentWriteStream.cs" />
+ <Compile Include="System\Net\Http\SocketsHttpHandler\HttpKeepAlivePingPolicy.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\IHttpTrace.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\IMultiWebProxy.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\MultiProxy.cs" />
Link="Common\System\Text\ValueStringBuilder.cs" />
<Compile Include="$(CommonPath)System\Threading\Tasks\TaskToApm.cs"
Link="System\System\Threading\Tasks\TaskToApm.cs" />
+ <Compile Include="System\Net\Http\SocketsHttpHandler\HttpKeepAlivePingPolicy.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpNoProxy.cs" />
<Compile Include="System\Net\Http\BrowserHttpHandler\SystemProxyInfo.Browser.cs" />
<Compile Include="System\Net\Http\BrowserHttpHandler\SocketsHttpHandler.cs" />
set => throw new PlatformNotSupportedException();
}
+ public TimeSpan KeepAlivePingDelay
+ {
+ get => throw new PlatformNotSupportedException();
+ set => throw new PlatformNotSupportedException();
+ }
+
+ public TimeSpan KeepAlivePingTimeout
+ {
+ get => throw new PlatformNotSupportedException();
+ set => throw new PlatformNotSupportedException();
+ }
+
+
+ public HttpKeepAlivePingPolicy KeepAlivePingPolicy
+ {
+ get => throw new PlatformNotSupportedException();
+ set => throw new PlatformNotSupportedException();
+ }
+
public ConnectionFactory? ConnectionFactory
{
get => throw new PlatformNotSupportedException();
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Threading;
+
+namespace System.Net.Http
+{
+ /// <summary>
+ /// Additional default values used used only in this assembly.
+ /// </summary>
+ internal static partial class HttpHandlerDefaults
+ {
+ public static readonly TimeSpan DefaultKeepAlivePingTimeout = TimeSpan.FromSeconds(20);
+ public static readonly TimeSpan DefaultKeepAlivePingDelay = Timeout.InfiniteTimeSpan;
+ public const HttpKeepAlivePingPolicy DefaultKeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always;
+ }
+}
// 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.Buffers.Binary;
using System.Collections.Generic;
using System.Diagnostics;
// Channel options for creating _writeChannel
private static readonly UnboundedChannelOptions s_channelOptions = new UnboundedChannelOptions() { SingleReader = true };
+ internal enum KeepAliveState
+ {
+ None,
+ PingSent
+ }
+
+ private readonly long _keepAlivePingDelay;
+ private readonly long _keepAlivePingTimeout;
+ private readonly HttpKeepAlivePingPolicy _keepAlivePingPolicy;
+ private long _keepAlivePingPayload;
+ private long _nextPingRequestTimestamp;
+ private long _keepAlivePingTimeoutTimestamp;
+ private volatile KeepAliveState _keepAliveState;
+
public Http2Connection(HttpConnectionPool pool, Connection connection)
{
_pool = pool;
_pendingWindowUpdate = 0;
_idleSinceTickCount = Environment.TickCount64;
+
+ _keepAlivePingDelay = TimeSpanToMs(_pool.Settings._keepAlivePingDelay);
+ _keepAlivePingTimeout = TimeSpanToMs(_pool.Settings._keepAlivePingTimeout);
+ _nextPingRequestTimestamp = Environment.TickCount64 + _keepAlivePingDelay;
+ _keepAlivePingPolicy = _pool.Settings._keepAlivePingPolicy;
+
if (NetEventSource.Log.IsEnabled()) TraceConnection(_stream);
+
+ static long TimeSpanToMs(TimeSpan value) {
+ double milliseconds = value.TotalMilliseconds;
+ return (long)(milliseconds > int.MaxValue ? int.MaxValue : milliseconds);
+ }
}
private object SyncObject => _httpStreams;
frameHeader = await ReadFrameAsync().ConfigureAwait(false);
if (NetEventSource.Log.IsEnabled()) Trace($"Frame {frameNum}: {frameHeader}.");
+ RefreshPingTimestamp();
+
// Process the frame.
switch (frameHeader.Type)
{
ThrowProtocolError();
}
- if (frameHeader.AckFlag)
- {
- // We never send PING, so an ACK indicates a protocol error
- ThrowProtocolError();
- }
-
if (frameHeader.PayloadLength != FrameHeader.PingLength)
{
ThrowProtocolError(Http2ProtocolErrorCode.FrameSizeError);
ReadOnlySpan<byte> pingContent = _incomingBuffer.ActiveSpan.Slice(0, FrameHeader.PingLength);
long pingContentLong = BinaryPrimitives.ReadInt64BigEndian(pingContent);
- LogExceptions(SendPingAckAsync(pingContentLong));
-
+ if (frameHeader.AckFlag)
+ {
+ ProcessPingAck(pingContentLong);
+ }
+ else
+ {
+ LogExceptions(SendPingAsync(pingContentLong, isAck: true));
+ }
_incomingBuffer.Discard(frameHeader.PayloadLength);
}
});
/// <param name="pingContent">The 8-byte ping content to send, read as a big-endian integer.</param>
- private Task SendPingAckAsync(long pingContent) =>
- PerformWriteAsync(FrameHeader.Size + FrameHeader.PingLength, (thisRef: this, pingContent), static (state, writeBuffer) =>
+ /// <param name="isAck">Determine whether the frame is ping or ping ack.</param>
+ private Task SendPingAsync(long pingContent, bool isAck = false) =>
+ PerformWriteAsync(FrameHeader.Size + FrameHeader.PingLength, (thisRef: this, pingContent, isAck), static (state, writeBuffer) =>
{
if (NetEventSource.Log.IsEnabled()) state.thisRef.Trace("Started writing.");
Debug.Assert(sizeof(long) == FrameHeader.PingLength);
Span<byte> span = writeBuffer.Span;
- FrameHeader.WriteTo(span, FrameHeader.PingLength, FrameType.Ping, FrameFlags.Ack, streamId: 0);
+ FrameHeader.WriteTo(span, FrameHeader.PingLength, FrameType.Ping, state.isAck ? FrameFlags.Ack: FrameFlags.None, streamId: 0);
BinaryPrimitives.WriteInt64BigEndian(span.Slice(FrameHeader.Size), state.pingContent);
return true;
return true;
});
+
+ internal void HeartBeat()
+ {
+ if (_disposed)
+ return;
+
+ try
+ {
+ VerifyKeepAlive();
+ }
+ catch (Exception e)
+ {
+ if (NetEventSource.Log.IsEnabled()) Trace($"{nameof(HeartBeat)}: {e.Message}");
+
+ Abort(e);
+ }
+ }
+
private static (ReadOnlyMemory<byte> first, ReadOnlyMemory<byte> rest) SplitBuffer(ReadOnlyMemory<byte> buffer, int maxSize) =>
buffer.Length > maxSize ?
(buffer.Slice(0, maxSize), buffer.Slice(maxSize)) :
_concurrentStreams.AdjustCredit(1);
}
+ private void RefreshPingTimestamp()
+ {
+ _nextPingRequestTimestamp = Environment.TickCount64 + _keepAlivePingDelay;
+ }
+
+ private void ProcessPingAck(long payload)
+ {
+ if (_keepAliveState != KeepAliveState.PingSent)
+ ThrowProtocolError();
+ if (Interlocked.Read(ref _keepAlivePingPayload) != payload)
+ ThrowProtocolError();
+ _keepAliveState = KeepAliveState.None;
+ }
+
+ private void VerifyKeepAlive()
+ {
+ if (_keepAlivePingPolicy == HttpKeepAlivePingPolicy.WithActiveRequests)
+ {
+ lock (SyncObject)
+ {
+ if (_httpStreams.Count == 0) return;
+ }
+ }
+
+ long now = Environment.TickCount64;
+ switch (_keepAliveState)
+ {
+ case KeepAliveState.None:
+ // Check whether keep alive delay has passed since last frame received
+ if (now > _nextPingRequestTimestamp)
+ {
+ // Set the status directly to ping sent and set the timestamp
+ _keepAliveState = KeepAliveState.PingSent;
+ _keepAlivePingTimeoutTimestamp = now + _keepAlivePingTimeout;
+
+ long pingPayload = Interlocked.Increment(ref _keepAlivePingPayload);
+ SendPingAsync(pingPayload);
+ return;
+ }
+ break;
+ case KeepAliveState.PingSent:
+ if (now > _keepAlivePingTimeoutTimestamp)
+ ThrowProtocolError();
+ break;
+ default:
+ Debug.Fail($"Unexpected keep alive state ({_keepAliveState})");
+ break;
+ }
+ }
+
public sealed override string ToString() => $"{nameof(Http2Connection)}({_pool})"; // Description for diagnostic purposes
public override void Trace(string message, [CallerMemberName] string? memberName = null) =>
return false;
}
+ internal void HeartBeat()
+ {
+ Http2Connection[]? localHttp2Connections = _http2Connections;
+ if (localHttp2Connections != null)
+ {
+ foreach (Http2Connection http2Connection in localHttp2Connections)
+ {
+ http2Connection.HeartBeat();
+ }
+ }
+ }
+
+
// For diagnostic purposes
public override string ToString() =>
$"{nameof(HttpConnectionPool)} " +
private readonly ConcurrentDictionary<HttpConnectionKey, HttpConnectionPool> _pools;
/// <summary>Timer used to initiate cleaning of the pools.</summary>
private readonly Timer? _cleaningTimer;
+ /// <summary>Heart beat timer currently used for Http2 ping only.</summary>
+ private readonly Timer? _heartBeatTimer;
/// <summary>The maximum number of connections allowed per pool. <see cref="int.MaxValue"/> indicates unlimited.</summary>
private readonly int _maxConnectionsPerServer;
// Temporary
// Create the timer. Ensure the Timer has a weak reference to this manager; otherwise, it
// can introduce a cycle that keeps the HttpConnectionPoolManager rooted by the Timer
// implementation until the handler is Disposed (or indefinitely if it's not).
+ var thisRef = new WeakReference<HttpConnectionPoolManager>(this);
+
_cleaningTimer = new Timer(static s =>
{
var wr = (WeakReference<HttpConnectionPoolManager>)s!;
{
thisRef.RemoveStalePools();
}
- }, new WeakReference<HttpConnectionPoolManager>(this), Timeout.Infinite, Timeout.Infinite);
+ }, thisRef, Timeout.Infinite, Timeout.Infinite);
+
+
+ // For now heart beat is used only for ping functionality.
+ if (_settings._keepAlivePingDelay != Timeout.InfiniteTimeSpan)
+ {
+ long heartBeatInterval = (long)Math.Max(1000, Math.Min(_settings._keepAlivePingDelay.TotalMilliseconds, _settings._keepAlivePingTimeout.TotalMilliseconds) / 4);
+
+ _heartBeatTimer = new Timer(static state =>
+ {
+ var wr = (WeakReference<HttpConnectionPoolManager>)state!;
+ if (wr.TryGetTarget(out HttpConnectionPoolManager? thisRef))
+ {
+ thisRef.HeartBeat();
+ }
+ }, thisRef, heartBeatInterval, heartBeatInterval);
+ }
}
finally
{
public void Dispose()
{
_cleaningTimer?.Dispose();
-
+ _heartBeatTimer?.Dispose();
foreach (KeyValuePair<HttpConnectionKey, HttpConnectionPool> pool in _pools)
{
pool.Value.Dispose();
// be returned to pools they weren't associated with.
}
+ private void HeartBeat()
+ {
+ foreach (KeyValuePair<HttpConnectionKey, HttpConnectionPool> pool in _pools)
+ {
+ pool.Value.HeartBeat();
+ }
+ }
+
private static string GetIdentityIfDefaultCredentialsUsed(bool defaultCredentialsUsed)
{
return defaultCredentialsUsed ? CurrentUserIdentityProvider.GetIdentity() : string.Empty;
internal TimeSpan _pooledConnectionLifetime = HttpHandlerDefaults.DefaultPooledConnectionLifetime;
internal TimeSpan _pooledConnectionIdleTimeout = HttpHandlerDefaults.DefaultPooledConnectionIdleTimeout;
internal TimeSpan _expect100ContinueTimeout = HttpHandlerDefaults.DefaultExpect100ContinueTimeout;
+ internal TimeSpan _keepAlivePingTimeout = HttpHandlerDefaults.DefaultKeepAlivePingTimeout;
+ internal TimeSpan _keepAlivePingDelay = HttpHandlerDefaults.DefaultKeepAlivePingDelay;
+ internal HttpKeepAlivePingPolicy _keepAlivePingPolicy = HttpHandlerDefaults.DefaultKeepAlivePingPolicy;
internal TimeSpan _connectTimeout = HttpHandlerDefaults.DefaultConnectTimeout;
internal HeaderEncodingSelector<HttpRequestMessage>? _requestHeaderEncodingSelector;
_sslOptions = _sslOptions?.ShallowClone(), // shallow clone the options for basic prevention of mutation issues while processing
_useCookies = _useCookies,
_useProxy = _useProxy,
+ _keepAlivePingTimeout = _keepAlivePingTimeout,
+ _keepAlivePingDelay = _keepAlivePingDelay,
+ _keepAlivePingPolicy = _keepAlivePingPolicy,
_requestHeaderEncodingSelector = _requestHeaderEncodingSelector,
_responseHeaderEncodingSelector = _responseHeaderEncodingSelector,
_enableMultipleHttp2Connections = _enableMultipleHttp2Connections,
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Net.Http
+{
+ public enum HttpKeepAlivePingPolicy
+ {
+ /// <summary>
+ /// Sends keep alive ping for only when there are active streams on the connection.
+ /// </summary>
+ WithActiveRequests,
+
+ /// <summary>
+ /// Sends keep alive ping for whole connection lifetime.
+ /// </summary>
+ Always
+ }
+}
}
/// <summary>
+ /// Gets or sets the keep alive ping delay. The client will send a keep alive ping to the server if it
+ /// doesn't receive any frames on a connection for this period of time. This property is used together with
+ /// <see cref="SocketsHttpHandler.KeepAlivePingTimeout"/> to close broken connections.
+ /// <para>
+ /// Delay value must be greater than or equal to 1 second. Set to <see cref="Timeout.InfiniteTimeSpan"/> to
+ /// disable the keep alive ping.
+ /// Defaults to <see cref="Timeout.InfiniteTimeSpan"/>.
+ /// </para>
+ /// </summary>
+ public TimeSpan KeepAlivePingDelay
+ {
+ get => _settings._keepAlivePingDelay;
+ set
+ {
+ if (value.Ticks < TimeSpan.TicksPerSecond && value != Timeout.InfiniteTimeSpan)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), value, SR.Format(SR.net_http_value_must_be_greater_than_or_equal, value, TimeSpan.FromSeconds(1)));
+ }
+
+ CheckDisposedOrStarted();
+ _settings._keepAlivePingDelay = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the keep alive ping timeout. Keep alive pings are sent when a period of inactivity exceeds
+ /// the configured <see cref="KeepAlivePingDelay"/> value. The client will close the connection if it
+ /// doesn't receive any frames within the timeout.
+ /// <para>
+ /// Timeout must be greater than or equal to 1 second. Set to <see cref="Timeout.InfiniteTimeSpan"/> to
+ /// disable the keep alive ping timeout.
+ /// Defaults to 20 seconds.
+ /// </para>
+ /// </summary>
+ public TimeSpan KeepAlivePingTimeout
+ {
+ get => _settings._keepAlivePingTimeout;
+ set
+ {
+ if (value.Ticks < TimeSpan.TicksPerSecond && value != Timeout.InfiniteTimeSpan)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), value, SR.Format(SR.net_http_value_must_be_greater_than_or_equal, value, TimeSpan.FromSeconds(1)));
+ }
+
+ CheckDisposedOrStarted();
+ _settings._keepAlivePingTimeout = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the keep alive ping behaviour. Keep alive pings are sent when a period of inactivity exceeds
+ /// the configured <see cref="KeepAlivePingDelay"/> value.
+ /// </summary>
+ public HttpKeepAlivePingPolicy KeepAlivePingPolicy
+ {
+ get => _settings._keepAlivePingPolicy;
+ set
+ {
+ CheckDisposedOrStarted();
+ _settings._keepAlivePingPolicy = value;
+ }
+ }
+
+ /// <summary>
/// Gets or sets a value that indicates whether additional HTTP/2 connections can be established to the same server
/// when the maximum of concurrent streams is reached on all existing connections.
/// </summary>
}
}
+ public static IEnumerable<object[]> KeepAliveTestDataSource()
+ {
+ yield return new object[] { Timeout.InfiniteTimeSpan, HttpKeepAlivePingPolicy.Always, false };
+ yield return new object[] { TimeSpan.FromSeconds(1), HttpKeepAlivePingPolicy.WithActiveRequests, false };
+ yield return new object[] { TimeSpan.FromSeconds(1), HttpKeepAlivePingPolicy.Always, false };
+ yield return new object[] { TimeSpan.FromSeconds(1), HttpKeepAlivePingPolicy.WithActiveRequests, true };
+ }
+
+ [OuterLoop("Significant delay.")]
+ [MemberData(nameof(KeepAliveTestDataSource))]
+ [ConditionalTheory(nameof(SupportsAlpn))]
+ public async Task Http2_PingKeepAlive(TimeSpan keepAlivePingDelay, HttpKeepAlivePingPolicy keepAlivePingPolicy, bool expectRequestFail)
+ {
+ TimeSpan pingTimeout = TimeSpan.FromSeconds(5);
+ // Simulate failure by delaying the pong, otherwise send it immediately.
+ TimeSpan pongDelay = expectRequestFail ? pingTimeout * 2 : TimeSpan.Zero;
+ // Pings are send only if KeepAlivePingDelay is not infinite.
+ bool expectStreamPing = keepAlivePingDelay != Timeout.InfiniteTimeSpan;
+ // Pings (regardless ongoing communication) are send only if sending is on and policy is set to always.
+ bool expectPingWithoutStream = expectStreamPing && keepAlivePingPolicy == HttpKeepAlivePingPolicy.Always;
+
+ TaskCompletionSource serverFinished = new TaskCompletionSource();
+
+ await Http2LoopbackServer.CreateClientAndServerAsync(
+ async uri =>
+ {
+ SocketsHttpHandler handler = new SocketsHttpHandler()
+ {
+ KeepAlivePingTimeout = pingTimeout,
+ KeepAlivePingPolicy = keepAlivePingPolicy,
+ KeepAlivePingDelay = keepAlivePingDelay
+ };
+ handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };
+
+ using HttpClient client = new HttpClient(handler);
+ client.DefaultRequestVersion = HttpVersion.Version20;
+
+ // Warmup request to create connection.
+ await client.GetStringAsync(uri);
+ // Request under the test scope.
+ if (expectRequestFail)
+ {
+ await Assert.ThrowsAsync<HttpRequestException>(() => client.GetStringAsync(uri));
+ // As stream is closed we don't want to continue with sending data.
+ return;
+ }
+ else
+ {
+ await client.GetStringAsync(uri);
+ }
+
+ // Let connection live until server finishes.
+ await serverFinished.Task.TimeoutAfter(pingTimeout * 2);
+ },
+ async server =>
+ {
+ using Http2LoopbackConnection connection = await server.EstablishConnectionAsync();
+
+ Task<PingFrame> receivePingTask = expectStreamPing ? connection.ExpectPingFrameAsync() : null;
+
+ // Warmup the connection.
+ int streamId1 = await connection.ReadRequestHeaderAsync();
+ await connection.SendDefaultResponseAsync(streamId1);
+
+ // Request under the test scope.
+ int streamId2 = await connection.ReadRequestHeaderAsync();
+
+ // Test ping with active stream.
+ if (!expectStreamPing)
+ {
+ await Assert.ThrowsAsync<OperationCanceledException>(() => connection.ReadPingAsync(pingTimeout));
+ }
+ else
+ {
+ PingFrame ping;
+ if (receivePingTask != null && receivePingTask.IsCompleted)
+ {
+ ping = await receivePingTask;
+ }
+ else
+ {
+ ping = await connection.ReadPingAsync(pingTimeout);
+ }
+ await Task.Delay(pongDelay);
+
+ await connection.SendPingAckAsync(ping.Data);
+ }
+
+ // Send response and close the stream.
+ if (expectRequestFail)
+ {
+ await Assert.ThrowsAsync<NetworkException>(() => connection.SendDefaultResponseAsync(streamId2));
+ // As stream is closed we don't want to continue with sending data.
+ return;
+ }
+ await connection.SendDefaultResponseAsync(streamId2);
+ // Test ping with no active stream.
+ if (expectPingWithoutStream)
+ {
+ PingFrame ping = await connection.ReadPingAsync(pingTimeout);
+ await connection.SendPingAckAsync(ping.Data);
+ }
+ else
+ {
+ await Assert.ThrowsAsync<OperationCanceledException>(() => connection.ReadPingAsync(pingTimeout));
+ }
+ serverFinished.SetResult();
+ await connection.WaitForClientDisconnectAsync(true);
+ });
+ }
+
[OuterLoop("Uses Task.Delay")]
[ConditionalFact(nameof(SupportsAlpn))]
public async Task Http2_MaxConcurrentStreams_LimitEnforced()
{
using HttpClientHandler handler = CreateHttpClientHandler();
handler.ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates;
-
+
var socketsHandler = (SocketsHttpHandler)GetUnderlyingSocketsHttpHandler(handler);
socketsHandler.ConnectionFactory = connectionFactory;
await connection.WriteFrameAsync(MakeDataFrame(streamId, DataBytes));
// Additional trailing header frame.
- await connection.SendResponseHeadersAsync(streamId, isTrailingHeader:true, headers: TrailingHeaders, endStream : true);
+ await connection.SendResponseHeadersAsync(streamId, isTrailingHeader: true, headers: TrailingHeaders, endStream: true);
HttpResponseMessage response = await sendTask;
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
await connection.SendDefaultResponseHeadersAsync(streamId);
await connection.WriteFrameAsync(MakeDataFrame(streamId, DataBytes));
// Additional trailing header frame with pseudo-headers again..
- await connection.SendResponseHeadersAsync(streamId, isTrailingHeader:false, headers: TrailingHeaders, endStream : true);
+ await connection.SendResponseHeadersAsync(streamId, isTrailingHeader: false, headers: TrailingHeaders, endStream: true);
await Assert.ThrowsAsync<HttpRequestException>(() => sendTask);
}
// Finish data stream and write out trailing headers.
await connection.WriteFrameAsync(MakeDataFrame(streamId, DataBytes));
- await connection.SendResponseHeadersAsync(streamId, endStream : true, isTrailingHeader:true, headers: TrailingHeaders);
+ await connection.SendResponseHeadersAsync(streamId, endStream: true, isTrailingHeader: true, headers: TrailingHeaders);
// Read data until EOF is reached
- while (stream.Read(data, 0, data.Length) != 0);
+ while (stream.Read(data, 0, data.Length) != 0) ;
Assert.Equal(TrailingHeaders.Count, response.TrailingHeaders.Count());
Assert.Contains("amazingtrailer", response.TrailingHeaders.GetValues("MyCoolTrailerHeader"));
// Response header.
await connection.SendDefaultResponseHeadersAsync(streamId);
- await connection.SendResponseHeadersAsync(streamId, endStream : true, isTrailingHeader:true, headers: TrailingHeaders);
+ await connection.SendResponseHeadersAsync(streamId, endStream: true, isTrailingHeader: true, headers: TrailingHeaders);
HttpResponseMessage response = await sendTask;
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
using (var serverStream = new NetworkStream(server, ownsSocket: false))
using (var serverReader = new StreamReader(serverStream))
{
- while (!string.IsNullOrWhiteSpace(await serverReader.ReadLineAsync()));
+ while (!string.IsNullOrWhiteSpace(await serverReader.ReadLineAsync())) ;
await server.SendAsync(new ArraySegment<byte>(Encoding.ASCII.GetBytes(responseBody)), SocketFlags.None);
await firstRequest;
Task<Socket> secondAccept = listener.AcceptAsync(); // shouldn't complete
Task<string> additionalRequest = client.GetStringAsync(uri);
- while (!string.IsNullOrWhiteSpace(await serverReader.ReadLineAsync()));
+ while (!string.IsNullOrWhiteSpace(await serverReader.ReadLineAsync())) ;
await server.SendAsync(new ArraySegment<byte>(Encoding.ASCII.GetBytes(responseBody)), SocketFlags.None);
await additionalRequest;
"Content-Length: 0\r\n" +
"\r\n";
- using (var handler = new HttpClientHandler())
+ using (var handler = new HttpClientHandler())
{
handler.Proxy = new UseSpecifiedUriWebProxy(proxyUrl, new NetworkCredential("abc", "password"));
// Get first request, no body for GET.
await connection.ReadRequestHeaderAndSendCustomResponseAsync(responseBody).ConfigureAwait(false);
// Client should send another request after being rejected with 407.
- await connection.ReadRequestHeaderAndSendResponseAsync(content:"OK").ConfigureAwait(false);
+ await connection.ReadRequestHeaderAndSendResponseAsync(content: "OK").ConfigureAwait(false);
});
string response = await request;
}
[Fact]
+ public void KeepAlivePing_GetSet_Roundtrips()
+ {
+ using var handler = new SocketsHttpHandler();
+
+ var testTimeSpanValue = TimeSpan.FromSeconds(5);
+ var invalidTimeSpanValue = TimeSpan.FromTicks(TimeSpan.TicksPerSecond - 1);
+
+ Assert.Equal(TimeSpan.FromSeconds(20), handler.KeepAlivePingTimeout);
+ handler.KeepAlivePingTimeout = testTimeSpanValue;
+ Assert.Equal(testTimeSpanValue, handler.KeepAlivePingTimeout);
+
+ Assert.Equal(Timeout.InfiniteTimeSpan, handler.KeepAlivePingDelay);
+ handler.KeepAlivePingDelay = testTimeSpanValue;
+ Assert.Equal(testTimeSpanValue, handler.KeepAlivePingDelay);
+
+ Assert.Equal(HttpKeepAlivePingPolicy.Always, handler.KeepAlivePingPolicy);
+ handler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.WithActiveRequests;
+ Assert.Equal(HttpKeepAlivePingPolicy.WithActiveRequests, handler.KeepAlivePingPolicy);
+
+ Assert.Throws<ArgumentOutOfRangeException>(() => handler.KeepAlivePingTimeout = invalidTimeSpanValue);
+ Assert.Throws<ArgumentOutOfRangeException>(() => handler.KeepAlivePingDelay = invalidTimeSpanValue);
+ }
+
+ [Fact]
public void MaxAutomaticRedirections_GetSet_Roundtrips()
{
using (var handler = new SocketsHttpHandler())
Assert.Throws(expectedExceptionType, () => handler.SslOptions = new SslClientAuthenticationOptions());
Assert.Throws(expectedExceptionType, () => handler.UseCookies = false);
Assert.Throws(expectedExceptionType, () => handler.UseProxy = false);
+ Assert.Throws(expectedExceptionType, () => handler.KeepAlivePingTimeout = TimeSpan.FromSeconds(5));
+ Assert.Throws(expectedExceptionType, () => handler.KeepAlivePingDelay = TimeSpan.FromSeconds(5));
+ Assert.Throws(expectedExceptionType, () => handler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.WithActiveRequests);
}
}
}
Link="ProductionCode\Common\System\Threading\Tasks\TaskToApm.cs" />
<Compile Include="..\..\src\System\Net\Http\SocketsHttpHandler\AuthenticationHelper.Digest.cs"
Link="ProductionCode\System\Net\Http\SocketsHttpHandler\AuthenticationHelper.Digest.cs" />
+ <Compile Include="..\..\src\System\Net\Http\SocketsHttpHandler\HttpKeepAlivePingPolicy.cs"
+ Link="ProductionCode\System\Net\Http\SocketsHttpHandler\HttpKeepAlivePingPolicy.cs" />
<Compile Include="..\..\src\System\Net\Http\HttpBaseStream.cs"
Link="ProductionCode\System\Net\Http\HttpBaseStream.cs" />
<Compile Include="..\..\src\System\Net\Http\ByteArrayContent.cs"
Link="ProductionCode\System\Net\Http\Headers\WarningHeaderValue.cs" />
<Compile Include="..\..\src\System\Net\Http\HttpClient.cs"
Link="ProductionCode\System\Net\Http\HttpClient.cs" />
+ <Compile Include="..\..\src\System\Net\Http\HttpHandlerDefaults.cs"
+ Link="ProductionCode\System\Net\Http\HttpHandlerDefaults.cs" />
<Compile Include="..\..\src\System\Net\Http\HttpCompletionOption.cs"
Link="ProductionCode\System\Net\Http\HttpCompletionOption.cs" />
<Compile Include="..\..\src\System\Net\Http\HttpContent.cs"