< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 204
Coverable lines: 204
Total lines: 382
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 118
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Diagnostics;
 5using System.Diagnostics.CodeAnalysis;
 6using System.IO;
 7using System.Net.Http.Headers;
 8using System.Net.Http.Metrics;
 9using System.Net.Security;
 10using System.Runtime.CompilerServices;
 11using System.Text;
 12using System.Threading;
 13using System.Threading.Tasks;
 14
 15namespace System.Net.Http
 16{
 17    internal abstract class HttpConnectionBase : IDisposable, IHttpTrace
 18    {
 19        protected readonly HttpConnectionPool _pool;
 20
 021        private static long s_connectionCounter = -1;
 22
 23        // May be null if none of the counters were enabled when the connection was established.
 24        private ConnectionMetrics? _connectionMetrics;
 25
 26        // Indicates whether we've counted this connection as established, so that we can
 27        // avoid decrementing the counter once it's closed in case telemetry was enabled in between.
 28        private bool _httpTelemetryMarkedConnectionAsOpened;
 29
 030        private readonly long _creationTickCount = Environment.TickCount64;
 31        private long? _idleSinceTickCount;
 32
 33        /// <summary>
 34        /// The context passed to the <see cref="SocketsHttpHandler.ShouldEvictConnection"/> callback.
 35        /// A single instance is reused to avoid allocating a new context for each eviction check.
 36        /// </summary>
 37        private SocketsHttpConnectionEvictionContext? _evictionContext;
 38
 39        /// <summary>
 40        /// Allocated at establishment only when <see cref="SocketsHttpHandler.ShouldEvictConnection"/>
 41        /// is configured, and canceled (but not disposed, so handed-out tokens stay valid) when the connection closes.
 42        /// </summary>
 43        private CancellationTokenSource? _connectionDisposalCts;
 44
 45        /// <summary>
 46        /// The <see cref="HttpConnectionPool.EvictionGeneration"/> at which this connection was last evaluated by the
 47        /// <see cref="SocketsHttpHandler.ShouldEvictConnection"/> callback.
 48        /// </summary>
 49        private int _lastEvictionGeneration;
 50
 51        /// <summary>
 52        /// Set while the <see cref="SocketsHttpHandler.ShouldEvictConnection"/> callback is running for this connection
 53        /// Ensures the callback is invoked for at most one caller at a time for a given connection.
 54        /// </summary>
 55        private bool _evictionCallbackInProgress;
 56
 57        private volatile bool _markedForEviction;
 58
 59        /// <summary>Cached string for the last Date header received on this connection.</summary>
 60        private string? _lastDateHeaderValue;
 61        /// <summary>Cached string for the last Server header received on this connection.</summary>
 62        private string? _lastServerHeaderValue;
 63
 64        /// <summary>Whether the connection has been marked for eviction by <see cref="SocketsHttpHandler.ShouldEvictCon
 065        public bool MarkedForEviction => _markedForEviction;
 66
 067        public long Id { get; }
 68
 069        public Activity? ConnectionSetupActivity { get; private set; }
 70
 071        public HttpConnectionBase(HttpConnectionPool pool, long connectionId)
 072        {
 073            Debug.Assert(this is HttpConnection or Http2Connection or Http3Connection);
 074            Debug.Assert(pool != null);
 075            _pool = pool;
 076            Id = connectionId;
 077        }
 78
 79        public HttpConnectionBase(HttpConnectionPool pool, long connectionId, Activity? connectionSetupActivity, IPEndPo
 080            : this(pool, connectionId)
 081        {
 82            // HTTP/1.1 and HTTP/2 connections always target the pool's origin authority.
 083            MarkConnectionAsEstablished(connectionSetupActivity, remoteEndPoint, pool.OriginAuthority);
 084        }
 85
 86        /// <summary>Allocates the next unique connection id. Generated before the connection object exists so that
 87        /// the same id can be surfaced to <see cref="SocketsHttpHandler.ConnectCallback"/> and telemetry.</summary>
 088        internal static long GetNextConnectionId() => Interlocked.Increment(ref s_connectionCounter);
 89
 90        protected void MarkConnectionAsEstablished(Activity? connectionSetupActivity, IPEndPoint? remoteEndPoint, HttpAu
 091        {
 092            ConnectionSetupActivity = connectionSetupActivity;
 93
 94            // The eviction generation baseline and disposal token are only relevant when a ShouldEvictConnection
 95            // callback is configured, so they're only set up in that case.
 096            if (_pool.Settings._shouldEvictConnection is not null)
 097            {
 98                // Baseline the eviction generation to the pool's current value so a freshly established connection isn'
 99                // immediately re-evaluated; it becomes eligible once the next maintenance pass advances the generation.
 0100                _lastEvictionGeneration = _pool.EvictionGeneration;
 101
 0102                _connectionDisposalCts = new CancellationTokenSource();
 103
 104                // Report the endpoint this connection actually targets, consistent with remoteEndPoint. HTTP/3 passes
 105                // the exact DnsEndPoint it used to establish the QuicConnection (which Alt-Svc may point at an authorit
 106                // distinct from the origin); HTTP/1.1 and HTTP/2 fall back to the pool's origin authority.
 0107                _evictionContext = new SocketsHttpConnectionEvictionContext(
 0108                    connectedEndPoint ?? new DnsEndPoint(authority.IdnHost, authority.Port),
 0109                    remoteEndPoint,
 0110                    Id,
 0111                    this is HttpConnection ? HttpVersion.Version11 : this is Http2Connection ? HttpVersion.Version20 : H
 0112                    _creationTickCount);
 0113            }
 114
 0115            if (GlobalHttpSettings.MetricsHandler.IsGloballyEnabled)
 0116            {
 0117                Debug.Assert(_pool.Settings._metrics is not null);
 118
 0119                SocketsHttpHandlerMetrics metrics = _pool.Settings._metrics!;
 0120                if (metrics.OpenConnections.Enabled || metrics.ConnectionDuration.Enabled)
 0121                {
 122                    // While requests may report HTTP/1.0 as the protocol, we treat all HTTP/1.X connections as HTTP/1.1
 0123                    string protocol =
 0124                        this is HttpConnection ? "1.1" :
 0125                        this is Http2Connection ? "2" :
 0126                        "3";
 127
 0128                    Debug.Assert(_pool.TelemetryServerAddress is not null, "TelemetryServerAddress should not be null wh
 0129                    _connectionMetrics = new ConnectionMetrics(
 0130                        metrics,
 0131                        protocol,
 0132                        _pool.IsSecure ? "https" : "http",
 0133                        _pool.TelemetryServerAddress,
 0134                        _pool.OriginAuthority.Port,
 0135                        remoteEndPoint?.Address?.ToString());
 136
 0137                    _connectionMetrics.ConnectionEstablished();
 0138                }
 0139            }
 140
 0141            _idleSinceTickCount = _creationTickCount;
 142
 0143            if (HttpTelemetry.Log.IsEnabled())
 0144            {
 0145                _httpTelemetryMarkedConnectionAsOpened = true;
 146
 0147                string scheme = _pool.IsSecure ? "https" : "http";
 0148                string host = _pool.OriginAuthority.HostValue;
 0149                int port = _pool.OriginAuthority.Port;
 150
 0151                if (this is HttpConnection) HttpTelemetry.Log.Http11ConnectionEstablished(Id, scheme, host, port, remote
 0152                else if (this is Http2Connection) HttpTelemetry.Log.Http20ConnectionEstablished(Id, scheme, host, port, 
 0153                else HttpTelemetry.Log.Http30ConnectionEstablished(Id, scheme, host, port, remoteEndPoint);
 0154            }
 0155        }
 156
 157        public void MarkConnectionAsClosed()
 0158        {
 159            // Cancel the disposal token used by the ShouldEvictConnection callback. The source is intentionally not
 160            // disposed so tokens already handed to a running callback stay valid (and observe the cancellation).
 0161            _connectionDisposalCts?.Cancel();
 162
 0163            if (GlobalHttpSettings.MetricsHandler.IsGloballyEnabled) _connectionMetrics?.ConnectionClosed(durationMs: En
 164
 0165            if (HttpTelemetry.Log.IsEnabled())
 0166            {
 167                // Only decrement the connection count if we counted this connection
 0168                if (_httpTelemetryMarkedConnectionAsOpened)
 0169                {
 0170                    if (this is HttpConnection) HttpTelemetry.Log.Http11ConnectionClosed(Id);
 0171                    else if (this is Http2Connection) HttpTelemetry.Log.Http20ConnectionClosed(Id);
 0172                    else HttpTelemetry.Log.Http30ConnectionClosed(Id);
 0173                }
 0174            }
 0175        }
 176
 177        public void MarkConnectionAsIdle()
 0178        {
 0179            _idleSinceTickCount = Environment.TickCount64;
 0180            if (GlobalHttpSettings.MetricsHandler.IsGloballyEnabled) _connectionMetrics?.IdleStateChanged(idle: true);
 0181        }
 182
 183        public void MarkConnectionAsNotIdle()
 0184        {
 0185            _idleSinceTickCount = null;
 0186            if (GlobalHttpSettings.MetricsHandler.IsGloballyEnabled) _connectionMetrics?.IdleStateChanged(idle: false);
 0187        }
 188
 189        /// <summary>Uses <see cref="HeaderDescriptor.GetHeaderValue"/>, but first special-cases several known headers f
 190        public string GetResponseHeaderValueWithCaching(HeaderDescriptor descriptor, ReadOnlySpan<byte> value, Encoding?
 0191        {
 0192            return
 0193                descriptor.Equals(KnownHeaders.Date) ? GetOrAddCachedValue(ref _lastDateHeaderValue, descriptor, value, 
 0194                descriptor.Equals(KnownHeaders.Server) ? GetOrAddCachedValue(ref _lastServerHeaderValue, descriptor, val
 0195                descriptor.GetHeaderValue(value, valueEncoding);
 196
 197            static string GetOrAddCachedValue([NotNull] ref string? cache, HeaderDescriptor descriptor, ReadOnlySpan<byt
 0198            {
 0199                string? lastValue = cache;
 0200                if (lastValue is null || !Ascii.Equals(value, lastValue))
 0201                {
 0202                    cache = lastValue = descriptor.GetHeaderValue(value, encoding);
 0203                }
 0204                Debug.Assert(cache is not null);
 0205                return lastValue;
 0206            }
 0207        }
 208
 209        public abstract void Trace(string message, [CallerMemberName] string? memberName = null);
 210
 211        protected void TraceConnection(Stream stream)
 0212        {
 0213            if (stream is SslStream sslStream)
 0214            {
 215#pragma warning disable SYSLIB0058 // Use NegotiatedCipherSuite.
 0216                Trace(
 0217                    $"{this}. Id:{Id}, " +
 0218                    $"SslProtocol:{sslStream.SslProtocol}, NegotiatedApplicationProtocol:{sslStream.NegotiatedApplicatio
 0219                    $"NegotiatedCipherSuite:{sslStream.NegotiatedCipherSuite}, CipherAlgorithm:{sslStream.CipherAlgorith
 0220                    $"HashAlgorithm:{sslStream.HashAlgorithm}, HashStrength:{sslStream.HashStrength}, " +
 0221                    $"KeyExchangeAlgorithm:{sslStream.KeyExchangeAlgorithm}, KeyExchangeStrength:{sslStream.KeyExchangeS
 0222                    $"LocalCertificate:{sslStream.LocalCertificate}, RemoteCertificate:{sslStream.RemoteCertificate}");
 223#pragma warning restore SYSLIB0058 // Use NegotiatedCipherSuite.
 0224            }
 225            else
 0226            {
 0227                Trace($"{this}. Id:{Id}");
 0228            }
 0229        }
 230
 0231        public long GetLifetimeTicks(long nowTicks) => nowTicks - _creationTickCount;
 232
 233        /// <summary>The amount of time that has elapsed since the connection was established.</summary>
 0234        internal TimeSpan Age => TimeSpan.FromMilliseconds(GetLifetimeTicks(Environment.TickCount64));
 235
 0236        public long GetIdleTicks(long nowTicks) => _idleSinceTickCount is long idleSinceTickCount ? nowTicks - idleSince
 237
 238        /// <summary>
 239        /// Called when a connection is returned to the pool to run the eviction evaluation that may have been skipped
 240        /// for it during a background pass. HTTP/1.1 connections that were in use at the time weren't visible in the
 241        /// available list (they were checked out as pending), so the pass couldn't evaluate them. Comparing the
 242        /// connection's last evaluated generation against the pool's current one detects that case and evaluates now.
 243        /// </summary>
 244        public void RunEvictionEvaluationIfNeeded()
 0245        {
 0246            if (_pool.EvictionGeneration != _lastEvictionGeneration)
 0247            {
 0248                _ = EvaluateForEvictionAsync();
 0249            }
 0250        }
 251
 252        /// <summary>
 253        /// Runs the <see cref="SocketsHttpHandler.ShouldEvictConnection"/> callback for this connection
 254        /// and marks the connection for eviction if the callback requests it. The callback runs at most
 255        /// once at a time for this connection.
 256        /// </summary>
 257        public async Task EvaluateForEvictionAsync()
 0258        {
 0259            Debug.Assert(_pool.Settings._shouldEvictConnection is not null);
 0260            Debug.Assert(_connectionDisposalCts is not null);
 0261            Debug.Assert(_evictionContext is not null);
 262
 263            // There's a benign race condition here where we might run the callback more than once for a given generatio
 0264            if (Interlocked.Exchange(ref _evictionCallbackInProgress, true) ||
 0265                MarkedForEviction ||
 0266                _connectionDisposalCts.IsCancellationRequested)
 0267            {
 0268                return;
 269            }
 270
 271            try
 0272            {
 0273                if (await _pool.Settings._shouldEvictConnection(_evictionContext, _connectionDisposalCts.Token).Configur
 0274                {
 0275                    _markedForEviction = true;
 276
 0277                    if (NetEventSource.Log.IsEnabled()) Trace("Marking connection for eviction per ShouldEvictConnection
 0278                }
 0279            }
 0280            catch (Exception e)
 0281            {
 282                // Don't let a misbehaving user callback take down pool maintenance.
 0283                if (NetEventSource.Log.IsEnabled()) Trace($"{nameof(SocketsHttpHandler.ShouldEvictConnection)} threw an 
 0284            }
 285            finally
 0286            {
 0287                _lastEvictionGeneration = _pool.EvictionGeneration;
 0288                _evictionCallbackInProgress = false;
 0289            }
 0290        }
 291
 292        /// <summary>Check whether a connection is still usable, or should be scavenged.</summary>
 293        /// <returns>True if connection can be used.</returns>
 0294        public virtual bool CheckUsabilityOnScavenge() => true;
 295
 0296        internal static bool IsDigit(byte c) => (uint)(c - '0') <= '9' - '0';
 297
 298        internal static int ParseStatusCode(ReadOnlySpan<byte> value)
 0299        {
 300            byte status1, status2, status3;
 0301            if (value.Length != 3 ||
 0302                !IsDigit(status1 = value[0]) ||
 0303                !IsDigit(status2 = value[1]) ||
 0304                !IsDigit(status3 = value[2]))
 0305            {
 0306                throw new HttpRequestException(HttpRequestError.InvalidResponse, SR.Format(SR.net_http_invalid_response_
 307            }
 308
 0309            return 100 * (status1 - '0') + 10 * (status2 - '0') + (status3 - '0');
 0310        }
 311
 312        /// <summary>Awaits a task, logging any resulting exceptions (which are otherwise ignored).</summary>
 313        internal void LogExceptions(Task task)
 0314        {
 0315            if (task.IsCompleted)
 0316            {
 0317                if (task.IsFaulted)
 0318                {
 0319                    LogFaulted(this, task);
 0320                }
 0321            }
 322            else
 0323            {
 0324                task.ContinueWith(static (t, state) => LogFaulted((HttpConnectionBase)state!, t), this,
 0325                    CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyO
 0326            }
 327
 328            static void LogFaulted(HttpConnectionBase connection, Task task)
 0329            {
 0330                Debug.Assert(task.IsFaulted);
 0331                Exception? e = task.Exception!.InnerException; // Access Exception even if not tracing, to avoid TaskSch
 0332                if (NetEventSource.Log.IsEnabled()) connection.Trace($"Exception from asynchronous processing: {e}");
 0333            }
 0334        }
 335
 336        public abstract void Dispose();
 337
 338        /// <summary>
 339        /// Called by <see cref="HttpConnectionPool.CleanCacheAndDisposeIfUnused"/> while holding the lock.
 340        /// </summary>
 341        public bool IsUsable(long nowTicks, TimeSpan pooledConnectionLifetime, TimeSpan pooledConnectionIdleTimeout)
 0342        {
 343            // The connection may have been marked for eviction by the ShouldEvictConnection callback.
 0344            if (MarkedForEviction)
 0345            {
 0346                if (NetEventSource.Log.IsEnabled()) Trace("Scavenging connection. Connection was evicted.");
 0347                return false;
 348            }
 349
 350            // Validate that the connection hasn't been idle in the pool for longer than is allowed.
 0351            if (pooledConnectionIdleTimeout != Timeout.InfiniteTimeSpan)
 0352            {
 0353                long idleTicks = GetIdleTicks(nowTicks);
 0354                if (idleTicks > pooledConnectionIdleTimeout.TotalMilliseconds)
 0355                {
 0356                    if (NetEventSource.Log.IsEnabled()) Trace($"Scavenging connection. Idle {TimeSpan.FromMilliseconds(i
 0357                    return false;
 358                }
 0359            }
 360
 361            // Validate that the connection lifetime has not been exceeded.
 0362            if (pooledConnectionLifetime != Timeout.InfiniteTimeSpan)
 0363            {
 0364                long lifetimeTicks = GetLifetimeTicks(nowTicks);
 0365                if (lifetimeTicks > pooledConnectionLifetime.TotalMilliseconds)
 0366                {
 0367                    if (NetEventSource.Log.IsEnabled()) Trace($"Scavenging connection. Lifetime {TimeSpan.FromMillisecon
 0368                    return false;
 369                }
 0370            }
 371
 0372            if (!CheckUsabilityOnScavenge())
 0373            {
 0374                if (NetEventSource.Log.IsEnabled()) Trace($"Scavenging connection. Keep-Alive timeout exceeded, unexpect
 0375                return false;
 376            }
 377
 0378            return true;
 0379        }
 380    }
 381}
 382