Configure ping in HTTP2 (#40257)
authorJan Jahoda <jajahoda@microsoft.com>
Thu, 13 Aug 2020 06:51:29 +0000 (08:51 +0200)
committerGitHub <noreply@github.com>
Thu, 13 Aug 2020 06:51:29 +0000 (08:51 +0200)
18 files changed:
src/libraries/Common/src/System/Net/Http/HttpHandlerDefaults.cs
src/libraries/Common/tests/System/Net/Http/Http2Frames.cs
src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs
src/libraries/System.Net.Http.WinHttpHandler/src/System.Net.Http.WinHttpHandler.csproj
src/libraries/System.Net.Http/ref/System.Net.Http.cs
src/libraries/System.Net.Http/src/Resources/Strings.resx
src/libraries/System.Net.Http/src/System.Net.Http.csproj
src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/SocketsHttpHandler.cs
src/libraries/System.Net.Http/src/System/Net/Http/HttpHandlerDefaults.cs [new file with mode: 0644]
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpKeepAlivePingPolicy.cs [new file with mode: 0644]
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs
src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs
src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs
src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj

index d42462f..5375192 100644 (file)
@@ -9,7 +9,7 @@ namespace System.Net.Http
     /// 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;
index 3195e23..214dab6 100644 (file)
@@ -344,9 +344,9 @@ namespace System.Net.Test.Common
 
     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;
@@ -354,7 +354,7 @@ namespace System.Net.Test.Common
 
         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);
         }
@@ -364,12 +364,12 @@ namespace System.Net.Test.Common
             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}";
         }
     }
 
index 846e2ac..08eee10 100644 (file)
@@ -24,6 +24,7 @@ namespace System.Net.Test.Common
         private Stream _connectionStream;
         private TaskCompletionSource<bool> _ignoredSettingsAckPromise;
         private bool _ignoreWindowUpdates;
+        private TaskCompletionSource<PingFrame> _expectPingFrame;
         private readonly TimeSpan _timeout;
         private int _lastStreamId;
 
@@ -186,6 +187,13 @@ namespace System.Net.Test.Common
                 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)
             {
@@ -245,6 +253,15 @@ namespace System.Net.Test.Common
             _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);
@@ -663,7 +680,7 @@ namespace System.Net.Test.Common
 
         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);
@@ -675,6 +692,27 @@ namespace System.Net.Test.Common
             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"
index 71f4b00..1a499a9 100644 (file)
@@ -34,9 +34,9 @@
     <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"
@@ -56,7 +56,7 @@
     <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"
@@ -74,7 +74,7 @@
     <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" />
index dbf1278..6a7cf68 100644 (file)
@@ -347,6 +347,9 @@ namespace System.Net.Http
         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 { } }
@@ -369,6 +372,11 @@ namespace System.Net.Http
         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) { }
index 61b45be..84bc59a 100644 (file)
@@ -1,17 +1,17 @@
 <?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>
index f7d8e23..ec91aeb 100644 (file)
   </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" />
index 2a1d63d..214f562 100644 (file)
@@ -130,6 +130,25 @@ namespace System.Net.Http
             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();
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpHandlerDefaults.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpHandlerDefaults.cs
new file mode 100644 (file)
index 0000000..21cc00a
--- /dev/null
@@ -0,0 +1,17 @@
+// 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;
+    }
+}
index 289fd7a..3aae8a9 100644 (file)
@@ -1,6 +1,7 @@
 // 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;
@@ -96,6 +97,20 @@ namespace System.Net.Http
         // 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;
@@ -119,7 +134,18 @@ namespace System.Net.Http
             _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;
@@ -298,6 +324,8 @@ namespace System.Net.Http
                     frameHeader = await ReadFrameAsync().ConfigureAwait(false);
                     if (NetEventSource.Log.IsEnabled()) Trace($"Frame {frameNum}: {frameHeader}.");
 
+                    RefreshPingTimestamp();
+
                     // Process the frame.
                     switch (frameHeader.Type)
                     {
@@ -667,12 +695,6 @@ namespace System.Net.Http
                 ThrowProtocolError();
             }
 
-            if (frameHeader.AckFlag)
-            {
-                // We never send PING, so an ACK indicates a protocol error
-                ThrowProtocolError();
-            }
-
             if (frameHeader.PayloadLength != FrameHeader.PingLength)
             {
                 ThrowProtocolError(Http2ProtocolErrorCode.FrameSizeError);
@@ -685,8 +707,14 @@ namespace System.Net.Http
             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);
         }
 
@@ -936,15 +964,16 @@ namespace System.Net.Http
             });
 
         /// <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;
@@ -962,6 +991,24 @@ namespace System.Net.Http
                 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)) :
@@ -1849,6 +1896,56 @@ namespace System.Net.Http
             _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) =>
index c74f32a..33a93b0 100644 (file)
@@ -1774,6 +1774,19 @@ namespace System.Net.Http
             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)} " +
index 601b583..290d295 100644 (file)
@@ -36,6 +36,8 @@ namespace System.Net.Http
         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
@@ -102,6 +104,8 @@ namespace System.Net.Http
                     // 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!;
@@ -109,7 +113,23 @@ namespace System.Net.Http
                         {
                             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
                 {
@@ -455,7 +475,7 @@ namespace System.Net.Http
         public void Dispose()
         {
             _cleaningTimer?.Dispose();
-
+            _heartBeatTimer?.Dispose();
             foreach (KeyValuePair<HttpConnectionKey, HttpConnectionPool> pool in _pools)
             {
                 pool.Value.Dispose();
@@ -519,6 +539,14 @@ namespace System.Net.Http
             // 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;
index e3231b1..3eefbd3 100644 (file)
@@ -42,6 +42,9 @@ namespace System.Net.Http
         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;
@@ -103,6 +106,9 @@ namespace System.Net.Http
                 _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,
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpKeepAlivePingPolicy.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpKeepAlivePingPolicy.cs
new file mode 100644 (file)
index 0000000..7eb9f45
--- /dev/null
@@ -0,0 +1,18 @@
+// 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
+    }
+}
index 6a8a3d5..fbe84c6 100644 (file)
@@ -281,6 +281,70 @@ namespace System.Net.Http
         }
 
         /// <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>
index 7b62dcc..46b39fc 100644 (file)
@@ -1451,6 +1451,117 @@ namespace System.Net.Http.Functional.Tests
             }
         }
 
+        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()
index b58fc6b..49266bd 100644 (file)
@@ -136,7 +136,7 @@ namespace System.Net.Http.Functional.Tests
             {
                 using HttpClientHandler handler = CreateHttpClientHandler();
                 handler.ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates;
-                
+
                 var socketsHandler = (SocketsHttpHandler)GetUnderlyingSocketsHttpHandler(handler);
                 socketsHandler.ConnectionFactory = connectionFactory;
 
@@ -872,7 +872,7 @@ namespace System.Net.Http.Functional.Tests
                 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);
@@ -898,7 +898,7 @@ namespace System.Net.Http.Functional.Tests
                 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);
             }
@@ -937,10 +937,10 @@ namespace System.Net.Http.Functional.Tests
 
                 // 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"));
@@ -962,7 +962,7 @@ namespace System.Net.Http.Functional.Tests
 
                 // 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);
@@ -1302,14 +1302,14 @@ namespace System.Net.Http.Functional.Tests
                 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;
 
@@ -1584,7 +1584,7 @@ namespace System.Net.Http.Functional.Tests
                         "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"));
 
@@ -1597,7 +1597,7 @@ namespace System.Net.Http.Functional.Tests
                             // 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;
@@ -1685,6 +1685,30 @@ namespace System.Net.Http.Functional.Tests
         }
 
         [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())
@@ -1930,6 +1954,9 @@ namespace System.Net.Http.Functional.Tests
                 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);
             }
         }
     }
index ea67ef0..0af91c2 100644 (file)
@@ -70,6 +70,8 @@
              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"