File: System\Net\Http\WinHttpHandler.cs
Web Access
Project: src\src\runtime\src\libraries\System.Net.Http.WinHttpHandler\src\System.Net.Http.WinHttpHandler.csproj (System.Net.Http.WinHttpHandler)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net.Http.Headers;
using System.Net.Security;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using SafeWinHttpHandle = Interop.WinHttp.SafeWinHttpHandle;

namespace System.Net.Http
{
    public enum WindowsProxyUsePolicy
    {
        DoNotUseProxy = 0, // Don't use a proxy at all.
        UseWinHttpProxy = 1, // Use configuration as specified by "netsh winhttp" machine config command. Automatic detect not supported.
        UseWinInetProxy = 2, // WPAD protocol and PAC files supported.
        UseCustomProxy = 3 // Use the custom proxy specified in the Proxy property.
    }

    public enum CookieUsePolicy
    {
        IgnoreCookies = 0,
        UseInternalCookieStoreOnly = 1,
        UseSpecifiedCookieContainer = 2
    }

    public class WinHttpHandler : HttpMessageHandler
    {
        // These are normally defined already in System.Net.Primitives as part of the HttpVersion type.
        // However, these are not part of 'netstandard'. WinHttpHandler currently builds against
        // 'netstandard' so we need to add these definitions here.
        internal static readonly Version HttpVersion20 = new Version(2, 0);
        internal static readonly Version HttpVersion30 = new Version(3, 0);
        internal static readonly Version HttpVersionUnknown = new Version(0, 0);
        internal static bool DefaultCertificateRevocationCheck { get; }

        internal static bool CertificateCachingAppContextSwitchEnabled { get; } = AppContext.TryGetSwitch("System.Net.Http.UseWinHttpCertificateCaching", out bool enabled) && enabled;
        private static readonly TimeSpan s_maxTimeout = TimeSpan.FromMilliseconds(int.MaxValue);

        private static readonly Lazy<bool> s_supportsTls13 = new Lazy<bool>(CheckTls13Support);
        private static readonly TimeSpan s_cleanCachedCertificateTimeout = TimeSpan.FromMilliseconds((int?)AppDomain.CurrentDomain.GetData("System.Net.Http.WinHttpCertificateCachingCleanupTimerInterval") ?? 60_000);
        private static readonly long s_staleTimeout = (long)(s_cleanCachedCertificateTimeout.TotalSeconds * Stopwatch.Frequency);

        [ThreadStatic]
        private static StringBuilder? t_requestHeadersBuilder;

        private readonly object _lockObject = new object();
        private bool _automaticRedirection = HttpHandlerDefaults.DefaultAutomaticRedirection;
        private int _maxAutomaticRedirections = HttpHandlerDefaults.DefaultMaxAutomaticRedirections;
        private DecompressionMethods _automaticDecompression = HttpHandlerDefaults.DefaultAutomaticDecompression;
        private CookieUsePolicy _cookieUsePolicy = CookieUsePolicy.UseInternalCookieStoreOnly;
        private CookieContainer? _cookieContainer;
        private bool _enableMultipleHttp2Connections;

        private SslProtocols _sslProtocols = SslProtocols.None; // Use most secure protocols available.
        private Func<
            HttpRequestMessage,
            X509Certificate2,
            X509Chain,
            SslPolicyErrors,
            bool>? _serverCertificateValidationCallback;
        private bool _checkCertificateRevocationList = DefaultCertificateRevocationCheck;
        private ClientCertificateOption _clientCertificateOption = ClientCertificateOption.Manual;
        private X509Certificate2Collection? _clientCertificates; // Only create collection when required.
        private ICredentials? _serverCredentials;
        private bool _preAuthenticate;
        private WindowsProxyUsePolicy _windowsProxyUsePolicy = WindowsProxyUsePolicy.UseWinHttpProxy;
        private ICredentials? _defaultProxyCredentials;
        private IWebProxy? _proxy;
        private int _maxConnectionsPerServer = int.MaxValue;
        private TimeSpan _sendTimeout = TimeSpan.FromSeconds(30);
        private TimeSpan _receiveHeadersTimeout = TimeSpan.FromSeconds(30);
        private TimeSpan _receiveDataTimeout = TimeSpan.FromSeconds(30);

        // Using OS defaults for "Keep-alive timeout" and "keep-alive interval"
        // as documented in https://learn.microsoft.com/windows/win32/winsock/sio-keepalive-vals#remarks
        private TimeSpan _tcpKeepAliveTime = TimeSpan.FromHours(2);
        private TimeSpan _tcpKeepAliveInterval = TimeSpan.FromSeconds(1);
        private bool _tcpKeepAliveEnabled;

        private int _maxResponseHeadersLength = HttpHandlerDefaults.DefaultMaxResponseHeadersLength;
        private int _maxResponseDrainSize = HttpHandlerDefaults.DefaultMaxResponseDrainSize;
        private volatile bool _operationStarted;
        private volatile int _disposed;
        private SafeWinHttpHandle? _sessionHandle;
        private readonly WinHttpAuthHelper _authHelper = new WinHttpAuthHelper();
        private readonly Timer? _certificateCleanupTimer;
        private bool _isTimerRunning;
        private readonly ConcurrentDictionary<CachedCertificateKey, CachedCertificateValue> _cachedCertificates = new();

        public WinHttpHandler()
        {
            if (CertificateCachingAppContextSwitchEnabled)
            {
                WeakReference<WinHttpHandler> thisRef = new(this);
                bool restoreFlow = false;
                try
                {
                    if (!ExecutionContext.IsFlowSuppressed())
                    {
                        ExecutionContext.SuppressFlow();
                        restoreFlow = true;
                    }

                    _certificateCleanupTimer = new Timer(
                    static s =>
                    {
                        if (((WeakReference<WinHttpHandler>)s!).TryGetTarget(out WinHttpHandler? thisRef))
                        {
                            thisRef.ClearStaleCertificates();
                        }
                    },
                    thisRef,
                    Timeout.Infinite,
                    Timeout.Infinite);
                }
                finally
                {
                    if (restoreFlow)
                    {
                        ExecutionContext.RestoreFlow();
                    }
                }
            }
        }

        #region Properties
        public bool AutomaticRedirection
        {
            get
            {
                return _automaticRedirection;
            }

            set
            {
                CheckDisposedOrStarted();
                _automaticRedirection = value;
            }
        }

        public int MaxAutomaticRedirections
        {
            get
            {
                return _maxAutomaticRedirections;
            }

            set
            {
                if (value <= 0)
                {
                    throw new ArgumentOutOfRangeException(
                        nameof(value),
                        value,
                        SR.Format(SR.net_http_value_must_be_greater_than, 0));
                }

                CheckDisposedOrStarted();
                _maxAutomaticRedirections = value;
            }
        }

        public DecompressionMethods AutomaticDecompression
        {
            get
            {
                return _automaticDecompression;
            }

            set
            {
                CheckDisposedOrStarted();
                _automaticDecompression = value;
            }
        }

        public CookieUsePolicy CookieUsePolicy
        {
            get
            {
                return _cookieUsePolicy;
            }

            set
            {
                if (value != CookieUsePolicy.IgnoreCookies
                    && value != CookieUsePolicy.UseInternalCookieStoreOnly
                    && value != CookieUsePolicy.UseSpecifiedCookieContainer)
                {
                    throw new ArgumentOutOfRangeException(nameof(value));
                }

                CheckDisposedOrStarted();
                _cookieUsePolicy = value;
            }
        }

        public CookieContainer? CookieContainer
        {
            get
            {
                return _cookieContainer;
            }

            set
            {
                CheckDisposedOrStarted();
                _cookieContainer = value;
            }
        }

        public SslProtocols SslProtocols
        {
            get
            {
                return _sslProtocols;
            }

            set
            {
                CheckDisposedOrStarted();
                _sslProtocols = value;
            }
        }


        public Func<
            HttpRequestMessage,
            X509Certificate2,
            X509Chain,
            SslPolicyErrors,
            bool>? ServerCertificateValidationCallback
        {
            get
            {
                return _serverCertificateValidationCallback;
            }

            set
            {
                CheckDisposedOrStarted();

                _serverCertificateValidationCallback = value;
            }
        }

        public bool CheckCertificateRevocationList
        {
            get
            {
                return _checkCertificateRevocationList;
            }

            set
            {
                CheckDisposedOrStarted();
                _checkCertificateRevocationList = value;
            }
        }

        public ClientCertificateOption ClientCertificateOption
        {
            get
            {
                return _clientCertificateOption;
            }

            set
            {
                if (value != ClientCertificateOption.Manual
                    && value != ClientCertificateOption.Automatic)
                {
                    throw new ArgumentOutOfRangeException(nameof(value));
                }

                CheckDisposedOrStarted();
                _clientCertificateOption = value;
            }
        }

        public X509Certificate2Collection ClientCertificates
        {
            get
            {
                if (_clientCertificateOption != ClientCertificateOption.Manual)
                {
                    throw new InvalidOperationException(SR.Format(SR.net_http_invalid_enable_first, "ClientCertificateOptions", "Manual"));
                }

                return _clientCertificates ??= new X509Certificate2Collection();
            }
        }

        public bool PreAuthenticate
        {
            get
            {
                return _preAuthenticate;
            }

            set
            {
                _preAuthenticate = value;
            }
        }

        public ICredentials? ServerCredentials
        {
            get
            {
                return _serverCredentials;
            }

            set
            {
                _serverCredentials = value;
            }
        }

        public WindowsProxyUsePolicy WindowsProxyUsePolicy
        {
            get
            {
                return _windowsProxyUsePolicy;
            }

            set
            {
                if (value != WindowsProxyUsePolicy.DoNotUseProxy &&
                    value != WindowsProxyUsePolicy.UseWinHttpProxy &&
                    value != WindowsProxyUsePolicy.UseWinInetProxy &&
                    value != WindowsProxyUsePolicy.UseCustomProxy)
                {
                    throw new ArgumentOutOfRangeException(nameof(value));
                }

                CheckDisposedOrStarted();
                _windowsProxyUsePolicy = value;
            }
        }

        public ICredentials? DefaultProxyCredentials
        {
            get
            {
                return _defaultProxyCredentials;
            }

            set
            {
                CheckDisposedOrStarted();
                _defaultProxyCredentials = value;
            }
        }

        public IWebProxy? Proxy
        {
            get
            {
                return _proxy;
            }

            set
            {
                CheckDisposedOrStarted();
                _proxy = value;
            }
        }

        public int MaxConnectionsPerServer
        {
            get
            {
                return _maxConnectionsPerServer;
            }

            set
            {
                if (value < 1)
                {
                    // In WinHTTP, setting this to 0 results in it being reset to 2.
                    // So, we'll only allow settings above 0.
                    throw new ArgumentOutOfRangeException(
                        nameof(value),
                        value,
                        SR.Format(SR.net_http_value_must_be_greater_than, 0));
                }

                CheckDisposedOrStarted();
                _maxConnectionsPerServer = value;
            }
        }

        public TimeSpan SendTimeout
        {
            get
            {
                return _sendTimeout;
            }

            set
            {
                CheckTimeSpanPropertyValue(value);
                CheckDisposedOrStarted();
                _sendTimeout = value;
            }
        }


        public TimeSpan ReceiveHeadersTimeout
        {
            get
            {
                return _receiveHeadersTimeout;
            }

            set
            {
                CheckTimeSpanPropertyValue(value);
                CheckDisposedOrStarted();
                _receiveHeadersTimeout = value;
            }
        }

        public TimeSpan ReceiveDataTimeout
        {
            get
            {
                return _receiveDataTimeout;
            }

            set
            {
                CheckTimeSpanPropertyValue(value);
                CheckDisposedOrStarted();
                _receiveDataTimeout = value;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether TCP keep-alive is enabled.
        /// </summary>
        /// <remarks>
        /// Only supported on Windows 10 version 2004 or newer.
        /// If enabled, the values of <see cref="TcpKeepAliveInterval" /> and <see cref="TcpKeepAliveTime"/> will be forwarded
        /// to set WINHTTP_OPTION_TCP_KEEPALIVE, enabling and configuring TCP keep-alive for the backing TCP socket.
        /// </remarks>
        public bool TcpKeepAliveEnabled
        {
            get
            {
                return _tcpKeepAliveEnabled;
            }
            set
            {
                CheckDisposedOrStarted();
                _tcpKeepAliveEnabled = value;
            }
        }

        /// <summary>
        /// Gets or sets the TCP keep-alive timeout.
        /// </summary>
        /// <remarks>
        /// Only supported on Windows 10 version 2004 or newer.
        /// Has no effect if <see cref="TcpKeepAliveEnabled"/> is <see langword="false" />.
        /// The default value of this property is 2 hours.
        /// </remarks>
        public TimeSpan TcpKeepAliveTime
        {
            get
            {
                return _tcpKeepAliveTime;
            }
            set
            {
                CheckTimeSpanPropertyValue(value);
                CheckDisposedOrStarted();
                _tcpKeepAliveTime = value;
            }
        }

        /// <summary>
        /// Gets or sets the TCP keep-alive interval.
        /// </summary>
        /// <remarks>
        /// Only supported on Windows 10 version 2004 or newer.
        /// Has no effect if <see cref="TcpKeepAliveEnabled"/> is <see langword="false" />.
        /// The default value of this property is 1 second.
        /// </remarks>
        public TimeSpan TcpKeepAliveInterval
        {
            get
            {
                return _tcpKeepAliveInterval;
            }
            set
            {
                CheckTimeSpanPropertyValue(value);
                CheckDisposedOrStarted();
                _tcpKeepAliveInterval = value;
            }
        }

        public int MaxResponseHeadersLength
        {
            get
            {
                return _maxResponseHeadersLength;
            }

            set
            {
                if (value <= 0)
                {
                    throw new ArgumentOutOfRangeException(
                        nameof(value),
                        value,
                        SR.Format(SR.net_http_value_must_be_greater_than, 0));
                }

                CheckDisposedOrStarted();
                _maxResponseHeadersLength = value;
            }
        }

        public int MaxResponseDrainSize
        {
            get
            {
                return _maxResponseDrainSize;
            }

            set
            {
                if (value <= 0)
                {
                    throw new ArgumentOutOfRangeException(
                        nameof(value),
                        value,
                        SR.Format(SR.net_http_value_must_be_greater_than, 0));
                }

                CheckDisposedOrStarted();
                _maxResponseDrainSize = value;
            }
        }

        public bool EnableMultipleHttp2Connections
        {
            get
            {
                return _enableMultipleHttp2Connections;
            }
            set
            {
                CheckDisposedOrStarted();
                _enableMultipleHttp2Connections = value;
            }
        }

        public IDictionary<string, object> Properties => field ??= new Dictionary<string, object>();
        #endregion

        protected override void Dispose(bool disposing)
        {
            if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0)
            {
                if (disposing)
                {
                    _sessionHandle?.Dispose();
                    _certificateCleanupTimer?.Dispose();
                }
            }

            base.Dispose(disposing);
        }

        protected override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            ArgumentNullException.ThrowIfNull(request);

            Uri? requestUri = request.RequestUri;
            if (requestUri is null || !requestUri.IsAbsoluteUri)
            {
                throw new InvalidOperationException(SR.net_http_client_invalid_requesturi);
            }

            if (requestUri.Scheme != Uri.UriSchemeHttp && requestUri.Scheme != Uri.UriSchemeHttps)
            {
                throw new NotSupportedException(SR.Format(SR.net_http_unsupported_requesturi_scheme, requestUri.Scheme));
            }

            // Check for invalid combinations of properties.
            if (_proxy != null && _windowsProxyUsePolicy != WindowsProxyUsePolicy.UseCustomProxy)
            {
                throw new InvalidOperationException(SR.net_http_invalid_proxyusepolicy);
            }

            if (_windowsProxyUsePolicy == WindowsProxyUsePolicy.UseCustomProxy && _proxy == null)
            {
                throw new InvalidOperationException(SR.net_http_invalid_proxy);
            }

            if (_cookieUsePolicy == CookieUsePolicy.UseSpecifiedCookieContainer &&
                _cookieContainer == null)
            {
                throw new InvalidOperationException(SR.net_http_invalid_cookiecontainer);
            }

            CheckDisposed();

            SetOperationStarted();

            TaskCompletionSource<HttpResponseMessage> tcs = new TaskCompletionSource<HttpResponseMessage>();

            // Create state object and save current values of handler settings.
            var state = new WinHttpRequestState();
            state.Tcs = tcs;
            state.CancellationToken = cancellationToken;
            state.RequestMessage = request;
            state.Handler = this;
            state.CheckCertificateRevocationList = _checkCertificateRevocationList;
            state.ServerCertificateValidationCallback = _serverCertificateValidationCallback;
            state.WindowsProxyUsePolicy = _windowsProxyUsePolicy;
            state.Proxy = _proxy;
            state.ServerCredentials = _serverCredentials;
            state.DefaultProxyCredentials = _defaultProxyCredentials;
            state.PreAuthenticate = _preAuthenticate;

            Task.Factory.StartNew(s =>
                {
                    var whrs = (WinHttpRequestState)s!;
                    _ = whrs.Handler!.StartRequestAsync(whrs);
                },
                state,
                CancellationToken.None,
                TaskCreationOptions.DenyChildAttach,
                TaskScheduler.Default);

            return tcs.Task;
        }

        private static WinHttpChunkMode GetChunkedModeForSend(HttpRequestMessage requestMessage)
        {
            WinHttpChunkMode chunkedMode = WinHttpChunkMode.None;

            if (requestMessage.Headers.TransferEncodingChunked.HasValue &&
                requestMessage.Headers.TransferEncodingChunked.Value)
            {
                chunkedMode = WinHttpChunkMode.Manual;
            }

            HttpContent? requestContent = requestMessage.Content;
            if (requestContent != null)
            {
                if (requestContent.Headers.ContentLength.HasValue)
                {
                    if (chunkedMode == WinHttpChunkMode.Manual)
                    {
                        // Deal with conflict between 'Content-Length' vs. 'Transfer-Encoding: chunked' semantics.
                        // Current .NET Desktop HttpClientHandler allows both headers to be specified but ends up
                        // stripping out 'Content-Length' and using chunked semantics.  WinHttpHandler will maintain
                        // the same behavior.
                        requestContent.Headers.ContentLength = null;
                    }
                }
                else
                {
                    if (chunkedMode == WinHttpChunkMode.None)
                    {
                        // Neither 'Content-Length' nor 'Transfer-Encoding: chunked' semantics was given.

                        if (requestMessage.Version >= HttpVersion20)
                        {
                            // HTTP/2 supports automatic chunking (streaming the request body without a length).
                            chunkedMode = WinHttpChunkMode.Automatic;
                        }
                        else
                        {
                            // Current .NET Desktop HttpClientHandler uses 'Content-Length' semantics and
                            // buffers the content as well in some cases.  But the WinHttpHandler can't access
                            // the protected internal TryComputeLength() method of the content.  So, it
                            // will use 'Transfer-Encoding: chunked' semantics.
                            chunkedMode = WinHttpChunkMode.Manual;
                            requestMessage.Headers.TransferEncodingChunked = true;
                        }
                    }
                }
            }
            else if (chunkedMode == WinHttpChunkMode.Manual)
            {
                throw new InvalidOperationException(SR.net_http_chunked_not_allowed_with_empty_content);
            }

            return chunkedMode;
        }

        private static void AddRequestHeaders(
            SafeWinHttpHandle requestHandle,
            HttpRequestMessage requestMessage,
            CookieContainer? cookies)
        {
            Debug.Assert(requestMessage.RequestUri != null);

            // Get a StringBuilder to use for creating the request headers.
            // We cache one in TLS to avoid creating a new one for each request.
            StringBuilder? requestHeadersBuffer = t_requestHeadersBuilder;
            if (requestHeadersBuffer != null)
            {
                requestHeadersBuffer.Clear();
            }
            else
            {
                t_requestHeadersBuilder = requestHeadersBuffer = new StringBuilder();
            }

            // Manually add cookies.
            if (cookies != null && cookies.Count > 0)
            {
                string? cookieHeader = WinHttpCookieContainerAdapter.GetCookieHeader(requestMessage.RequestUri, cookies);
                if (!string.IsNullOrEmpty(cookieHeader))
                {
                    requestHeadersBuffer.AppendLine(cookieHeader);
                }
            }

            // Serialize general request headers.
            requestHeadersBuffer.AppendLine(requestMessage.Headers.ToString());

            // Serialize entity-body (content) headers.
            if (requestMessage.Content != null)
            {
                // TODO https://github.com/dotnet/runtime/issues/16162:
                // Content-Length header isn't getting correctly placed using ToString()
                // This is a bug in HttpContentHeaders that needs to be fixed.
                if (requestMessage.Content.Headers.ContentLength.HasValue)
                {
                    long contentLength = requestMessage.Content.Headers.ContentLength.Value;
                    requestMessage.Content.Headers.ContentLength = null;
                    requestMessage.Content.Headers.ContentLength = contentLength;
                }

                requestHeadersBuffer.AppendLine(requestMessage.Content.Headers.ToString());
            }

            // Add request headers to WinHTTP request handle.
            if (!Interop.WinHttp.WinHttpAddRequestHeaders(
                requestHandle,
                requestHeadersBuffer,
                (uint)requestHeadersBuffer.Length,
                Interop.WinHttp.WINHTTP_ADDREQ_FLAG_ADD))
            {
                throw WinHttpException.CreateExceptionUsingLastError(nameof(Interop.WinHttp.WinHttpAddRequestHeaders));
            }
        }

        private void EnsureSessionHandleExists(WinHttpRequestState state)
        {
            if (_sessionHandle == null)
            {
                lock (_lockObject)
                {
                    if (_sessionHandle == null)
                    {
                        SafeWinHttpHandle sessionHandle;
                        uint accessType;

                        // If a custom proxy is specified and it is really the system web proxy
                        // (initial WebRequest.DefaultWebProxy) then we need to update the settings
                        // since that object is only a sentinel.
                        if (state.WindowsProxyUsePolicy == WindowsProxyUsePolicy.UseCustomProxy)
                        {
                            Debug.Assert(state.Proxy != null);
                            Debug.Assert(state.RequestMessage != null);
                            Debug.Assert(state.RequestMessage.RequestUri != null);
                            try
                            {
                                state.Proxy.GetProxy(state.RequestMessage.RequestUri);
                            }
                            catch (PlatformNotSupportedException)
                            {
                                // This is the system web proxy.
                                state.WindowsProxyUsePolicy = WindowsProxyUsePolicy.UseWinInetProxy;
                                state.Proxy = null;
                            }
                        }

                        if (state.WindowsProxyUsePolicy == WindowsProxyUsePolicy.DoNotUseProxy ||
                            state.WindowsProxyUsePolicy == WindowsProxyUsePolicy.UseCustomProxy)
                        {
                            // Either no proxy at all or a custom IWebProxy proxy is specified.
                            // For a custom IWebProxy, we'll need to calculate and set the proxy
                            // on a per request handle basis using the request Uri.  For now,
                            // we set the session handle to have no proxy.
                            accessType = Interop.WinHttp.WINHTTP_ACCESS_TYPE_NO_PROXY;
                        }
                        else if (state.WindowsProxyUsePolicy == WindowsProxyUsePolicy.UseWinHttpProxy)
                        {
                            // Use WinHTTP per-machine proxy settings which are set using the "netsh winhttp" command.
                            accessType = Interop.WinHttp.WINHTTP_ACCESS_TYPE_DEFAULT_PROXY;
                        }
                        else
                        {
                            // Use WinInet per-user proxy settings.
                            accessType = Interop.WinHttp.WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY;
                        }

                        if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Proxy accessType={accessType}");

                        sessionHandle = Interop.WinHttp.WinHttpOpen(
                            IntPtr.Zero,
                            accessType,
                            Interop.WinHttp.WINHTTP_NO_PROXY_NAME,
                            Interop.WinHttp.WINHTTP_NO_PROXY_BYPASS,
                            (int)Interop.WinHttp.WINHTTP_FLAG_ASYNC);
                        ThrowOnInvalidHandle(sessionHandle, nameof(Interop.WinHttp.WinHttpOpen));

                        uint optionAssuredNonBlockingTrue = 1; // TRUE

                        if (!Interop.WinHttp.WinHttpSetOption(
                            sessionHandle,
                            Interop.WinHttp.WINHTTP_OPTION_ASSURED_NON_BLOCKING_CALLBACKS,
                            ref optionAssuredNonBlockingTrue,
                            (uint)sizeof(uint)))
                        {
                            // This option is not available on downlevel Windows versions. While it improves
                            // performance, we can ignore the error that the option is not available.
                            int lastError = Marshal.GetLastWin32Error();
                            if (lastError != Interop.WinHttp.ERROR_WINHTTP_INVALID_OPTION)
                            {
                                throw WinHttpException.CreateExceptionUsingError(lastError, nameof(Interop.WinHttp.WinHttpSetOption));
                            }
                        }

                        SetSessionHandleOptions(sessionHandle);
                        _sessionHandle = sessionHandle;
                    }
                }
            }
        }

        private async Task StartRequestAsync(WinHttpRequestState state)
        {
            Debug.Assert(state.RequestMessage != null);
            Debug.Assert(state.RequestMessage.RequestUri != null);
            Debug.Assert(state.Handler != null);
            Debug.Assert(state.Tcs != null);

            if (state.CancellationToken.IsCancellationRequested)
            {
                state.Tcs.TrySetCanceled(state.CancellationToken);
                state.ClearSendRequestState();
                return;
            }

            if (state.RequestMessage.Version != HttpVersion.Version10 && state.RequestMessage.Version != HttpVersion.Version11
                && state.RequestMessage.Version != HttpVersion20 && state.RequestMessage.Version != HttpVersion30)
            {
                state.Tcs.TrySetException(new NotSupportedException(SR.net_http_unsupported_version));
                return;
            }

            Task? sendRequestBodyTask = null;
            SafeWinHttpHandle? connectHandle = null;
            try
            {
                EnsureSessionHandleExists(state);
                Debug.Assert(_sessionHandle != null);

                SetEnableHttp2PlusClientCertificate(state.RequestMessage.RequestUri, state.RequestMessage.Version);

                // Specify an HTTP server.
                connectHandle = Interop.WinHttp.WinHttpConnect(
                    _sessionHandle,
                    state.RequestMessage.RequestUri.HostNameType == UriHostNameType.IPv6 ? "[" + state.RequestMessage.RequestUri.IdnHost + "]" : state.RequestMessage.RequestUri.IdnHost,
                    (ushort)state.RequestMessage.RequestUri.Port,
                    0);
                ThrowOnInvalidHandle(connectHandle, nameof(Interop.WinHttp.WinHttpConnect));
                connectHandle.SetParentHandle(_sessionHandle);

                // Try to use the requested version if a known/supported version was explicitly requested.
                // Otherwise, we simply use winhttp's default.
                string? httpVersion = null;
                if (state.RequestMessage.Version == HttpVersion.Version10)
                {
                    httpVersion = "HTTP/1.0";
                }
                else if (state.RequestMessage.Version == HttpVersion.Version11)
                {
                    httpVersion = "HTTP/1.1";
                }

                OpenRequestHandle(state, connectHandle, httpVersion, out WinHttpChunkMode chunkedModeForSend, out SafeWinHttpHandle requestHandle);
                state.RequestHandle = requestHandle;
                state.RequestHandle.SetParentHandle(connectHandle);

                // Set callback function.
                SetStatusCallback(state.RequestHandle, WinHttpRequestCallback.StaticCallbackDelegate);

                // Set needed options on the request handle.
                SetRequestHandleOptions(state);

                AddRequestHeaders(
                    state.RequestHandle,
                    state.RequestMessage,
                    _cookieUsePolicy == CookieUsePolicy.UseSpecifiedCookieContainer ? _cookieContainer : null);

                uint proxyAuthScheme = 0;
                uint serverAuthScheme = 0;
                state.RetryRequest = false;

                // The only way to abort pending async operations in WinHTTP is to close the WinHTTP handle.
                // We will detect a cancellation request on the cancellation token by registering a callback.
                // If the callback is invoked, then we begin the abort process by disposing the handle. This
                // will have the side-effect of WinHTTP cancelling any pending I/O and accelerating its callbacks
                // on the handle and thus releasing the awaiting tasks in the loop below. This helps to provide
                // a more timely, cooperative, cancellation pattern.
                using (state.CancellationToken.Register(static s =>
                {
                    var state = (WinHttpRequestState)s!;
                    lock (state.Lock)
                    {
                        state.RequestHandle?.Dispose();
                    }
                }, state))
                {
                    do
                    {
                        _authHelper.PreAuthenticateRequest(state, proxyAuthScheme);

                        await InternalSendRequestAsync(state);

                        RendezvousAwaitable<int> receivedResponseTask;

                        if (chunkedModeForSend == WinHttpChunkMode.Automatic)
                        {
                            // Start waiting to receive response headers before sending request body.
                            // This order is important because the response could be returned immediately
                            // with END_STREAM flag on headers. Trying to send request body after that
                            // can cause the request to go into a bad state.
                            //
                            // We only use this order if chunk mode is automatic because Windows versions
                            // prior to AUTOMATIC_CHUNKING didn't support it.
                            receivedResponseTask = InternalReceiveResponseHeadersAsync(state);

                            if (state.RequestMessage.Content != null)
                            {
                                sendRequestBodyTask = InternalSendRequestBodyAsync(state, chunkedModeForSend);
                            }
                        }
                        else
                        {
                            if (state.RequestMessage.Content != null)
                            {
                                sendRequestBodyTask = InternalSendRequestBodyAsync(state, chunkedModeForSend);
                                await sendRequestBodyTask.ConfigureAwait(false);
                            }

                            receivedResponseTask = InternalReceiveResponseHeadersAsync(state);
                        }

                        bool receivedResponse = await receivedResponseTask != 0;
                        if (receivedResponse)
                        {
                            // If we're manually handling cookies, we need to add them to the container after
                            // each response has been received.
                            if (state.Handler.CookieUsePolicy == CookieUsePolicy.UseSpecifiedCookieContainer)
                            {
                                WinHttpCookieContainerAdapter.AddResponseCookiesToContainer(state);
                            }

                            _authHelper.CheckResponseForAuthentication(
                                state,
                                ref proxyAuthScheme,
                                ref serverAuthScheme);
                        }
                    } while (state.RetryRequest);
                }

                state.CancellationToken.ThrowIfCancellationRequested();

                // Since the headers have been read, set the "receive" timeout to be based on each read
                // call of the response body data. WINHTTP_OPTION_RECEIVE_TIMEOUT sets a timeout on each
                // lower layer winsock read.
                // Timeout.InfiniteTimeSpan will be converted to uint.MaxValue milliseconds (~ 50 days).
                // The result a of double->uint cast is unspecified for -1 and may differ on ARM, returning 0 instead of uint.MaxValue.
                // To handle Timeout.InfiniteTimespan correctly, we need to cast to int first.
                uint optionData = (uint)(int)_receiveDataTimeout.TotalMilliseconds;
                SetWinHttpOption(state.RequestHandle, Interop.WinHttp.WINHTTP_OPTION_RECEIVE_TIMEOUT, ref optionData);

                HttpResponseMessage responseMessage = WinHttpResponseParser.CreateResponseMessage(state);
                state.Tcs.TrySetResult(responseMessage);

                // HttpStatusCode cast is needed for 308 Moved Permenantly, which we support but is not included in NetStandard status codes.
                if (NetEventSource.Log.IsEnabled() &&
                    ((responseMessage.StatusCode >= HttpStatusCode.MultipleChoices && responseMessage.StatusCode <= HttpStatusCode.SeeOther) ||
                     (responseMessage.StatusCode >= HttpStatusCode.RedirectKeepVerb && responseMessage.StatusCode <= (HttpStatusCode)308)) &&
                    state.RequestMessage.RequestUri.Scheme == Uri.UriSchemeHttps && responseMessage.Headers.Location?.Scheme == Uri.UriSchemeHttp)
                {
                    NetEventSource.Error(this, $"Insecure https to http redirect from {state.RequestMessage.RequestUri} to {responseMessage.Headers.Location} blocked.");
                }
            }
            catch (Exception ex)
            {
                HandleAsyncException(state, state.SavedException ?? ex);
            }
            finally
            {
                connectHandle?.Dispose();

                try
                {
                    // Wait for request body to finish sending.
                    if (sendRequestBodyTask != null)
                    {
                        await sendRequestBodyTask.ConfigureAwait(false);
                    }
                }
                finally
                {
                    state.ClearSendRequestState();
                }
            }
        }

        private void OpenRequestHandle(WinHttpRequestState state, SafeWinHttpHandle connectHandle, string? httpVersion, out WinHttpChunkMode chunkedModeForSend, out SafeWinHttpHandle requestHandle)
        {
            Debug.Assert(state.RequestMessage != null);
            Debug.Assert(state.RequestMessage.RequestUri != null);
            chunkedModeForSend = GetChunkedModeForSend(state.RequestMessage);

            // Create an HTTP request handle.
            requestHandle = Interop.WinHttp.WinHttpOpenRequest(
                connectHandle,
                state.RequestMessage.Method.Method,
                state.RequestMessage.RequestUri.PathAndQuery,
                httpVersion,
                Interop.WinHttp.WINHTTP_NO_REFERER,
                Interop.WinHttp.WINHTTP_DEFAULT_ACCEPT_TYPES,
                GetRequestFlags(state, chunkedModeForSend));

            // It is possible the request was made with the WINHTTP_FLAG_AUTOMATIC_CHUNKING flag
            // and the platform doesn't support that flag.
            if (requestHandle.IsInvalid)
            {
                int lastError = Marshal.GetLastWin32Error();
                if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, $"error={lastError}");
                if (lastError != Interop.WinHttp.ERROR_INVALID_PARAMETER || chunkedModeForSend != WinHttpChunkMode.Automatic)
                {
                    ThrowOnInvalidHandle(requestHandle, nameof(Interop.WinHttp.WinHttpOpenRequest));
                }

                // Platform doesn't support WINHTTP_FLAG_AUTOMATIC_CHUNKING. Revert to manual chunking.
                // Note that manual chunking with WinHttp downgrades HTTP/2 requests to HTTP/1.1.
                chunkedModeForSend = WinHttpChunkMode.Manual;
                state.RequestMessage.Headers.TransferEncodingChunked = true;

                requestHandle = Interop.WinHttp.WinHttpOpenRequest(
                    connectHandle,
                    state.RequestMessage.Method.Method,
                    state.RequestMessage.RequestUri.PathAndQuery,
                    httpVersion,
                    Interop.WinHttp.WINHTTP_NO_REFERER,
                    Interop.WinHttp.WINHTTP_DEFAULT_ACCEPT_TYPES,
                    GetRequestFlags(state, chunkedModeForSend));

                ThrowOnInvalidHandle(requestHandle, nameof(Interop.WinHttp.WinHttpOpenRequest));
            }

            static uint GetRequestFlags(WinHttpRequestState state, WinHttpChunkMode chunkedModeForSend)
            {
                // Turn off additional URI reserved character escaping (percent-encoding). This matches
                // .NET Framework behavior. System.Uri establishes the baseline rules for percent-encoding
                // of reserved characters.
                uint flags = Interop.WinHttp.WINHTTP_FLAG_ESCAPE_DISABLE;
                if (state.RequestMessage!.RequestUri!.Scheme == UriScheme.Https)
                {
                    flags |= Interop.WinHttp.WINHTTP_FLAG_SECURE;
                }
                if (chunkedModeForSend == WinHttpChunkMode.Automatic)
                {
                    flags |= Interop.WinHttp.WINHTTP_FLAG_AUTOMATIC_CHUNKING;
                }

                return flags;
            }
        }

        private void SetSessionHandleOptions(SafeWinHttpHandle sessionHandle)
        {
            SetSessionHandleConnectionOptions(sessionHandle);
            SetSessionHandleTlsOptions(sessionHandle);
            SetSessionHandleTimeoutOptions(sessionHandle);
            SetDisableHttp2StreamQueue(sessionHandle);
            SetTcpKeepalive(sessionHandle);
            SetRequireStreamEnd(sessionHandle);
        }

        private unsafe void SetTcpKeepalive(SafeWinHttpHandle sessionHandle)
        {
            if (_tcpKeepAliveEnabled)
            {
                var tcpKeepalive = new Interop.WinHttp.tcp_keepalive
                {
                    onoff = 1,

                    // Timeout.InfiniteTimeSpan will be converted to uint.MaxValue milliseconds (~ 50 days)
                    // The result a of double->uint cast is unspecified for -1 and may differ on ARM, returning 0 instead of uint.MaxValue.
                    // To handle Timeout.InfiniteTimespan correctly, we need to cast to int first.
                    keepaliveinterval = (uint)(int)_tcpKeepAliveInterval.TotalMilliseconds,
                    keepalivetime = (uint)(int)_tcpKeepAliveTime.TotalMilliseconds
                };

                SetWinHttpOption(
                    sessionHandle,
                    Interop.WinHttp.WINHTTP_OPTION_TCP_KEEPALIVE,
                    (IntPtr)(&tcpKeepalive),
                    (uint)sizeof(Interop.WinHttp.tcp_keepalive));
            }
        }

        private void SetRequireStreamEnd(SafeWinHttpHandle sessionHandle)
        {
            if (WinHttpTrailersHelper.OsSupportsTrailers)
            {
                // Setting WINHTTP_OPTION_REQUIRE_STREAM_END to TRUE is needed for WinHttp to read trailing headers
                // in case the response has Content-Length defined.
                // According to the WinHttp team, the feature-detection logic in WinHttpTrailersHelper.OsSupportsTrailers
                // should also indicate the support of WINHTTP_OPTION_REQUIRE_STREAM_END.
                // WINHTTP_OPTION_REQUIRE_STREAM_END doesn't have effect on HTTP 1.1 requests, therefore it's safe to set it on
                // the session handle so it is inhereted by all request handles.
                uint optionData = 1;
                if (!Interop.WinHttp.WinHttpSetOption(sessionHandle, Interop.WinHttp.WINHTTP_OPTION_REQUIRE_STREAM_END, ref optionData))
                {
                    if (NetEventSource.Log.IsEnabled())
                    {
                        NetEventSource.Info(this, "Failed to enable WINHTTP_OPTION_REQUIRE_STREAM_END error code: " + Marshal.GetLastWin32Error());
                    }
                }
            }
        }

        private void SetSessionHandleConnectionOptions(SafeWinHttpHandle sessionHandle)
        {
            uint optionData = (uint)_maxConnectionsPerServer;
            SetWinHttpOption(sessionHandle, Interop.WinHttp.WINHTTP_OPTION_MAX_CONNS_PER_SERVER, ref optionData);
            SetWinHttpOption(sessionHandle, Interop.WinHttp.WINHTTP_OPTION_MAX_CONNS_PER_1_0_SERVER, ref optionData);
        }

        private void SetSessionHandleTlsOptions(SafeWinHttpHandle sessionHandle)
        {
            const SslProtocols Tls13 = (SslProtocols)12288; // enum is missing in .NET Standard
            uint optionData = 0;

            if (_sslProtocols == SslProtocols.None)
            {
                return;
            }

#pragma warning disable 0618 // SSL2/SSL3 are deprecated
            if ((_sslProtocols & SslProtocols.Ssl2) != 0)
            {
                optionData |= Interop.WinHttp.WINHTTP_FLAG_SECURE_PROTOCOL_SSL2;
            }

            if ((_sslProtocols & SslProtocols.Ssl3) != 0)
            {
                optionData |= Interop.WinHttp.WINHTTP_FLAG_SECURE_PROTOCOL_SSL3;
            }
#pragma warning restore 0618

#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete
            if ((_sslProtocols & SslProtocols.Tls) != 0)
            {
                optionData |= Interop.WinHttp.WINHTTP_FLAG_SECURE_PROTOCOL_TLS1;
            }

            if ((_sslProtocols & SslProtocols.Tls11) != 0)
            {
                optionData |= Interop.WinHttp.WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1;
            }
#pragma warning restore SYSLIB0039

            if ((_sslProtocols & SslProtocols.Tls12) != 0)
            {
                optionData |= Interop.WinHttp.WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2;
            }

            // Set this only if supported by WinHttp version.
            if (s_supportsTls13.Value && (_sslProtocols & Tls13) != 0)
            {
                optionData |= Interop.WinHttp.WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_3;
            }

            // If only unknown values were asked for, report ERROR_INVALID_PARAMETER.
            if (optionData == 0)
            {
                throw WinHttpException.CreateExceptionUsingError(
                    unchecked((int)Interop.WinHttp.ERROR_INVALID_PARAMETER),
                    nameof(SetSessionHandleTlsOptions));
            }

            SetWinHttpOption(sessionHandle, Interop.WinHttp.WINHTTP_OPTION_SECURE_PROTOCOLS, ref optionData);
        }

        private static bool CheckTls13Support()
        {
            try
            {
                using (var handler = new WinHttpHandler())
                using (SafeWinHttpHandle sessionHandle = Interop.WinHttp.WinHttpOpen(
                            IntPtr.Zero,
                            Interop.WinHttp.WINHTTP_ACCESS_TYPE_NO_PROXY,
                            Interop.WinHttp.WINHTTP_NO_PROXY_NAME,
                            Interop.WinHttp.WINHTTP_NO_PROXY_BYPASS,
                            (int)Interop.WinHttp.WINHTTP_FLAG_ASYNC))
                {
                    uint optionData = Interop.WinHttp.WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_3;

                    handler.SetWinHttpOption(sessionHandle, Interop.WinHttp.WINHTTP_OPTION_SECURE_PROTOCOLS, ref optionData);
                    return true;
                }
            }
            catch
            {
                return false;
            }
        }

        private void SetSessionHandleTimeoutOptions(SafeWinHttpHandle sessionHandle)
        {
            if (!Interop.WinHttp.WinHttpSetTimeouts(
                sessionHandle,
                0,
                0,
                (int)_sendTimeout.TotalMilliseconds,
                (int)_receiveHeadersTimeout.TotalMilliseconds))
            {
                WinHttpException.ThrowExceptionUsingLastError(nameof(Interop.WinHttp.WinHttpSetTimeouts));
            }
        }

        private void SetRequestHandleOptions(WinHttpRequestState state)
        {
            Debug.Assert(state.RequestHandle != null);
            Debug.Assert(state.RequestMessage != null);
            Debug.Assert(state.RequestMessage.RequestUri != null);

            SetRequestHandleProxyOptions(state);
            SetRequestHandleDecompressionOptions(state.RequestHandle);
            SetRequestHandleRedirectionOptions(state.RequestHandle);
            SetRequestHandleCookieOptions(state.RequestHandle);
            SetRequestHandleTlsOptions(state.RequestHandle);
            SetRequestHandleClientCertificateOptions(state.RequestHandle, state.RequestMessage.RequestUri);
            SetRequestHandleCredentialsOptions(state);
            SetRequestHandleBufferingOptions(state.RequestHandle);
            SetRequestHandleHttp2Options(state.RequestHandle, state.RequestMessage.Version);
        }

        private static void SetRequestHandleProxyOptions(WinHttpRequestState state)
        {
            Debug.Assert(state.RequestMessage != null);
            Debug.Assert(state.RequestMessage.RequestUri != null);
            Debug.Assert(state.RequestHandle != null);

            // We've already set the proxy on the session handle if we're using no proxy or default/auto proxy settings.
            // We only need to change it on the request handle if we have a specific custom IWebProxy.
            if (state.WindowsProxyUsePolicy == WindowsProxyUsePolicy.UseCustomProxy)
            {
                Interop.WinHttp.WINHTTP_PROXY_INFO proxyInfo = default;
                bool updateProxySettings = false;
                Uri uri = state.RequestMessage.RequestUri;

                try
                {
                    if (state.Proxy != null)
                    {
                        updateProxySettings = true;

                        Uri? proxyUri = state.Proxy.IsBypassed(uri) ? null : state.Proxy.GetProxy(uri);
                        if (proxyUri == null)
                        {
                            proxyInfo.AccessType = Interop.WinHttp.WINHTTP_ACCESS_TYPE_NO_PROXY;
                        }
                        else
                        {
                            proxyInfo.AccessType = Interop.WinHttp.WINHTTP_ACCESS_TYPE_NAMED_PROXY;
                            string proxyString = proxyUri.Scheme + "://" + proxyUri.Authority;
                            proxyInfo.Proxy = Marshal.StringToHGlobalUni(proxyString);
                        }
                    }

                    if (updateProxySettings)
                    {
                        GCHandle pinnedHandle = GCHandle.Alloc(proxyInfo, GCHandleType.Pinned);

                        try
                        {
                            SetWinHttpOption(
                                state.RequestHandle,
                                Interop.WinHttp.WINHTTP_OPTION_PROXY,
                                pinnedHandle.AddrOfPinnedObject(),
                                (uint)Marshal.SizeOf(proxyInfo));
                        }
                        finally
                        {
                            pinnedHandle.Free();
                        }
                    }
                }
                finally
                {
                    Marshal.FreeHGlobal(proxyInfo.Proxy);
                    Marshal.FreeHGlobal(proxyInfo.ProxyBypass);
                }
            }
        }

        private void SetRequestHandleDecompressionOptions(SafeWinHttpHandle requestHandle)
        {
            uint optionData = 0;

            if (_automaticDecompression != DecompressionMethods.None)
            {
                if ((_automaticDecompression & DecompressionMethods.GZip) != 0)
                {
                    optionData |= Interop.WinHttp.WINHTTP_DECOMPRESSION_FLAG_GZIP;
                }

                if ((_automaticDecompression & DecompressionMethods.Deflate) != 0)
                {
                    optionData |= Interop.WinHttp.WINHTTP_DECOMPRESSION_FLAG_DEFLATE;
                }

                SetWinHttpOption(requestHandle, Interop.WinHttp.WINHTTP_OPTION_DECOMPRESSION, ref optionData);
            }
        }

        private void SetRequestHandleRedirectionOptions(SafeWinHttpHandle requestHandle)
        {
            uint optionData = 0;

            if (_automaticRedirection)
            {
                optionData = (uint)_maxAutomaticRedirections;
                SetWinHttpOption(
                    requestHandle,
                    Interop.WinHttp.WINHTTP_OPTION_MAX_HTTP_AUTOMATIC_REDIRECTS,
                    ref optionData);
            }

            optionData = _automaticRedirection ?
                Interop.WinHttp.WINHTTP_OPTION_REDIRECT_POLICY_DISALLOW_HTTPS_TO_HTTP :
                Interop.WinHttp.WINHTTP_OPTION_REDIRECT_POLICY_NEVER;
            SetWinHttpOption(requestHandle, Interop.WinHttp.WINHTTP_OPTION_REDIRECT_POLICY, ref optionData);
        }

        private void SetRequestHandleCookieOptions(SafeWinHttpHandle requestHandle)
        {
            if (_cookieUsePolicy == CookieUsePolicy.UseSpecifiedCookieContainer ||
                _cookieUsePolicy == CookieUsePolicy.IgnoreCookies)
            {
                uint optionData = Interop.WinHttp.WINHTTP_DISABLE_COOKIES;
                SetWinHttpOption(requestHandle, Interop.WinHttp.WINHTTP_OPTION_DISABLE_FEATURE, ref optionData);
            }
        }

        private void SetRequestHandleTlsOptions(SafeWinHttpHandle requestHandle)
        {
            // If we have a custom server certificate validation callback method then
            // we need to have WinHTTP ignore some errors so that the callback method
            // will have a chance to be called.
            uint optionData;
            if (_serverCertificateValidationCallback != null)
            {
                optionData =
                    Interop.WinHttp.SECURITY_FLAG_IGNORE_UNKNOWN_CA |
                    Interop.WinHttp.SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE |
                    Interop.WinHttp.SECURITY_FLAG_IGNORE_CERT_CN_INVALID |
                    Interop.WinHttp.SECURITY_FLAG_IGNORE_CERT_DATE_INVALID;
                SetWinHttpOption(requestHandle, Interop.WinHttp.WINHTTP_OPTION_SECURITY_FLAGS, ref optionData);
            }
            else if (_checkCertificateRevocationList)
            {
                // If no custom validation method, then we let WinHTTP do the revocation check itself.
                optionData = Interop.WinHttp.WINHTTP_ENABLE_SSL_REVOCATION;
                SetWinHttpOption(requestHandle, Interop.WinHttp.WINHTTP_OPTION_ENABLE_FEATURE, ref optionData);
            }
        }

        private void SetRequestHandleClientCertificateOptions(SafeWinHttpHandle requestHandle, Uri requestUri)
        {
            if (requestUri.Scheme != UriScheme.Https)
            {
                return;
            }

            X509Certificate2? clientCertificate;
            if (_clientCertificateOption == ClientCertificateOption.Manual)
            {
                clientCertificate = CertificateHelper.GetEligibleClientCertificate(ClientCertificates);
            }
            else
            {
                clientCertificate = CertificateHelper.GetEligibleClientCertificate();
            }

            if (clientCertificate != null)
            {
                SetWinHttpOption(
                    requestHandle,
                    Interop.WinHttp.WINHTTP_OPTION_CLIENT_CERT_CONTEXT,
                    clientCertificate.Handle,
                    (uint)Marshal.SizeOf<Interop.Crypt32.CERT_CONTEXT>());
            }
            else
            {
                SetNoClientCertificate(requestHandle);
            }
        }

        private void SetEnableHttp2PlusClientCertificate(Uri requestUri, Version requestVersion)
        {
            Debug.Assert(_sessionHandle != null);

            if (requestUri.Scheme != UriScheme.Https || requestVersion != HttpVersion20)
            {
                return;
            }

            // Newer versions of WinHTTP fully support HTTP/2 with TLS client certificates.
            // But the support must be opted in.
            uint optionData = Interop.WinHttp.WINHTTP_HTTP2_PLUS_CLIENT_CERT_FLAG;
            if (Interop.WinHttp.WinHttpSetOption(
                _sessionHandle,
                Interop.WinHttp.WINHTTP_OPTION_ENABLE_HTTP2_PLUS_CLIENT_CERT,
                ref optionData))
            {
                if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "HTTP/2 with TLS client cert supported");
            }
            else
            {
                if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "HTTP/2 with TLS client cert not supported");
            }
        }

        private void SetDisableHttp2StreamQueue(SafeWinHttpHandle sessionHandle)
        {
            if (_enableMultipleHttp2Connections)
            {
                uint optionData = 1;
                if (Interop.WinHttp.WinHttpSetOption(sessionHandle, Interop.WinHttp.WINHTTP_OPTION_DISABLE_STREAM_QUEUE, ref optionData))
                {
                    if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "Multiple HTTP/2 connections enabled.");
                }
                else
                {
                    if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "Multiple HTTP/2 connections cannot be enabled.");
                }
            }
        }

        internal static void SetNoClientCertificate(SafeWinHttpHandle requestHandle)
        {
            SetWinHttpOption(
                requestHandle,
                Interop.WinHttp.WINHTTP_OPTION_CLIENT_CERT_CONTEXT,
                IntPtr.Zero,
                0);
        }

        private void SetRequestHandleCredentialsOptions(WinHttpRequestState state)
        {
            Debug.Assert(state.RequestHandle != null);
            // By default, WinHTTP sets the default credentials policy such that it automatically sends default credentials
            // (current user's logged on Windows credentials) to a proxy when needed (407 response). It only sends
            // default credentials to a server (401 response) if the server is considered to be on the Intranet.
            // WinHttpHandler uses a more granual opt-in model for using default credentials that can be different between
            // proxy and server credentials. It will explicitly allow default credentials to be sent at a later stage in
            // the request processing (after getting a 401/407 response) when the proxy or server credential is set as
            // CredentialCache.DefaultNetworkCredential. For now, we set the policy to prevent any default credentials
            // from being automatically sent until we get a 401/407 response.
            _authHelper.ChangeDefaultCredentialsPolicy(
                state.RequestHandle,
                Interop.WinHttp.WINHTTP_AUTH_TARGET_SERVER,
                allowDefaultCredentials: false);
        }

        private void SetRequestHandleBufferingOptions(SafeWinHttpHandle requestHandle)
        {
            uint optionData = (uint)(_maxResponseHeadersLength * 1024);
            SetWinHttpOption(requestHandle, Interop.WinHttp.WINHTTP_OPTION_MAX_RESPONSE_HEADER_SIZE, ref optionData);
            optionData = (uint)_maxResponseDrainSize;
            SetWinHttpOption(requestHandle, Interop.WinHttp.WINHTTP_OPTION_MAX_RESPONSE_DRAIN_SIZE, ref optionData);
        }

        private void SetRequestHandleHttp2Options(SafeWinHttpHandle requestHandle, Version requestVersion)
        {
            Debug.Assert(requestHandle != null);
            uint optionData = (requestVersion == HttpVersion20) ? Interop.WinHttp.WINHTTP_PROTOCOL_FLAG_HTTP2 : 0;
            if (Interop.WinHttp.WinHttpSetOption(
                requestHandle,
                Interop.WinHttp.WINHTTP_OPTION_ENABLE_HTTP_PROTOCOL,
                ref optionData))
            {
                if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"HTTP/2 option supported, setting to {optionData}");
            }
            else
            {
                if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "HTTP/2 option not supported");
            }
        }

        private void SetWinHttpOption(SafeWinHttpHandle handle, uint option, ref uint optionData)
        {
            Debug.Assert(handle != null);
            if (!Interop.WinHttp.WinHttpSetOption(
                handle,
                option,
                ref optionData))
            {
                WinHttpException.ThrowExceptionUsingLastError(nameof(Interop.WinHttp.WinHttpSetOption));
            }
        }

        private static void SetWinHttpOption(
            SafeWinHttpHandle handle,
            uint option,
            IntPtr optionData,
            uint optionSize)
        {
            Debug.Assert(handle != null);
            if (!Interop.WinHttp.WinHttpSetOption(
                handle,
                option,
                optionData,
                optionSize))
            {
                WinHttpException.ThrowExceptionUsingLastError(nameof(Interop.WinHttp.WinHttpSetOption));
            }
        }

        private static void HandleAsyncException(WinHttpRequestState state, Exception ex)
        {
            Debug.Assert(state.Tcs != null);
            if (state.CancellationToken.IsCancellationRequested)
            {
                // If the exception was due to the cancellation token being canceled, throw cancellation exception.
                state.Tcs.TrySetCanceled(state.CancellationToken);
            }
            else if (ex is WinHttpException || ex is IOException || ex is InvalidOperationException)
            {
                // Wrap expected exceptions as HttpRequestExceptions since this is considered an error during
                // execution. All other exception types, including ArgumentExceptions and ProtocolViolationExceptions
                // are 'unexpected' or caused by user error and should not be wrapped.
                state.Tcs.TrySetException(new HttpRequestException(SR.net_http_client_execution_error, ex));
            }
            else
            {
                state.Tcs.TrySetException(ex);
            }
        }

        private void SetOperationStarted()
        {
            if (!_operationStarted)
            {
                _operationStarted = true;
            }
        }

        private void CheckDisposed()
        {
            if (_disposed == 1)
            {
                throw new ObjectDisposedException(GetType().FullName);
            }
        }

        private void CheckDisposedOrStarted()
        {
            CheckDisposed();
            if (_operationStarted)
            {
                throw new InvalidOperationException(SR.net_http_operation_started);
            }
        }

        private static void CheckTimeSpanPropertyValue(TimeSpan timeSpan)
        {
            if (timeSpan != Timeout.InfiniteTimeSpan && (timeSpan <= TimeSpan.Zero || timeSpan > s_maxTimeout))
            {
                throw new ArgumentOutOfRangeException("value");
            }
        }

        private void SetStatusCallback(
            SafeWinHttpHandle requestHandle,
            Interop.WinHttp.WINHTTP_STATUS_CALLBACK callback)
        {
            const uint notificationFlags =
                Interop.WinHttp.WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS |
                Interop.WinHttp.WINHTTP_CALLBACK_FLAG_HANDLES |
                Interop.WinHttp.WINHTTP_CALLBACK_FLAG_REDIRECT |
                Interop.WinHttp.WINHTTP_CALLBACK_FLAG_SEND_REQUEST |
                Interop.WinHttp.WINHTTP_CALLBACK_STATUS_CONNECTED_TO_SERVER;

            IntPtr oldCallback = Interop.WinHttp.WinHttpSetStatusCallback(
                requestHandle,
                callback,
                notificationFlags,
                IntPtr.Zero);

            if (oldCallback == new IntPtr(Interop.WinHttp.WINHTTP_INVALID_STATUS_CALLBACK))
            {
                int lastError = Marshal.GetLastWin32Error();
                if (lastError != Interop.WinHttp.ERROR_INVALID_HANDLE) // Ignore error if handle was already closed.
                {
                    throw WinHttpException.CreateExceptionUsingError(lastError, nameof(Interop.WinHttp.WinHttpSetStatusCallback));
                }
            }
        }

        private void ThrowOnInvalidHandle(SafeWinHttpHandle handle, string nameOfCalledFunction)
        {
            if (handle.IsInvalid)
            {
                int lastError = Marshal.GetLastWin32Error();
                if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, $"error={lastError}");

                handle.Dispose();

                throw WinHttpException.CreateExceptionUsingError(lastError, nameOfCalledFunction);
            }
        }

        private RendezvousAwaitable<int> InternalSendRequestAsync(WinHttpRequestState state)
        {
            lock (state.Lock)
            {
                Debug.Assert(state.RequestHandle != null);

                state.Pin();
                if (!Interop.WinHttp.WinHttpSendRequest(
                    state.RequestHandle,
                    IntPtr.Zero,
                    0,
                    IntPtr.Zero,
                    0,
                    0,
                    state.ToIntPtr()))
                {
                    // WinHTTP doesn't always associate our context value (state object) to the request handle.
                    // And thus we might not get a HANDLE_CLOSING notification which would normally cause the
                    // state object to be unpinned and disposed. So, we manually dispose the request handle and
                    // state object here.
                    state.RequestHandle.Dispose();
                    state.Dispose();
                    WinHttpException.ThrowExceptionUsingLastError(nameof(Interop.WinHttp.WinHttpSendRequest));
                }
            }

            return state.LifecycleAwaitable;
        }

        private static async Task InternalSendRequestBodyAsync(WinHttpRequestState state, WinHttpChunkMode chunkedModeForSend)
        {
            Debug.Assert(state.RequestMessage != null);
            Debug.Assert(state.RequestMessage.Content != null);

            using (var requestStream = new WinHttpRequestStream(state, chunkedModeForSend))
            {
                await state.RequestMessage.Content.CopyToAsync(requestStream, state.TransportContext).ConfigureAwait(false);
                await requestStream.EndUploadAsync().ConfigureAwait(false);
            }
        }

        private RendezvousAwaitable<int> InternalReceiveResponseHeadersAsync(WinHttpRequestState state)
        {
            lock (state.Lock)
            {
                Debug.Assert(state.RequestHandle != null);

                if (!Interop.WinHttp.WinHttpReceiveResponse(state.RequestHandle, IntPtr.Zero))
                {
                    throw WinHttpException.CreateExceptionUsingLastError(nameof(Interop.WinHttp.WinHttpReceiveResponse));
                }
            }

            return state.LifecycleAwaitable;
        }

        internal bool GetCertificateFromCache(CachedCertificateKey key, [NotNullWhen(true)] out byte[]? rawCertificateBytes)
        {
            if (_cachedCertificates.TryGetValue(key, out CachedCertificateValue? cachedValue))
            {
                cachedValue.LastUsedTime = Stopwatch.GetTimestamp();
                rawCertificateBytes = cachedValue.RawCertificateData;
                return true;
            }

            rawCertificateBytes = null;
            return false;
        }

        internal void AddCertificateToCache(CachedCertificateKey key, byte[] rawCertificateData)
        {
            if (_cachedCertificates.TryAdd(key, new CachedCertificateValue(rawCertificateData, Stopwatch.GetTimestamp())))
            {
                EnsureCleanupTimerRunning();
            }
        }

        internal bool TryRemoveCertificateFromCache(CachedCertificateKey key)
        {
            bool result = _cachedCertificates.TryRemove(key, out _);
            if (result)
            {
                StopCleanupTimerIfEmpty();
            }
            return result;
        }

        private void ChangeCleanerTimer(TimeSpan timeout)
        {
            Debug.Assert(Monitor.IsEntered(_lockObject));
            Debug.Assert(_certificateCleanupTimer != null);
            if (_certificateCleanupTimer!.Change(timeout, Timeout.InfiniteTimeSpan))
            {
                _isTimerRunning = timeout != Timeout.InfiniteTimeSpan;
            }
        }

        private void ClearStaleCertificates()
        {
            foreach (KeyValuePair<CachedCertificateKey, CachedCertificateValue> kvPair in _cachedCertificates)
            {
                if (IsStale(kvPair.Value.LastUsedTime))
                {
                    _cachedCertificates.TryRemove(kvPair.Key, out _);
                }
            }

            lock (_lockObject)
            {
                ChangeCleanerTimer(_cachedCertificates.IsEmpty ? Timeout.InfiniteTimeSpan : s_cleanCachedCertificateTimeout);
            }

            static bool IsStale(long lastUsedTime)
            {
                long now = Stopwatch.GetTimestamp();
                return (now - lastUsedTime) > s_staleTimeout;
            }
        }

        private void EnsureCleanupTimerRunning()
        {
            lock (_lockObject)
            {
                if (!_cachedCertificates.IsEmpty && !_isTimerRunning)
                {
                    ChangeCleanerTimer(s_cleanCachedCertificateTimeout);
                }
            }
        }

        private void StopCleanupTimerIfEmpty()
        {
            lock (_lockObject)
            {
                if (_cachedCertificates.IsEmpty && _isTimerRunning)
                {
                    ChangeCleanerTimer(Timeout.InfiniteTimeSpan);
                }
            }
        }
    }
}