public byte[] Body;
public string Method;
public string Path;
+ public Version Version;
public List<HttpHeaderData> Headers { get; }
public int RequestId; // Generic request ID. Currently only used for HTTP/2 to hold StreamId.
var result = new HttpRequestData();
result.Method = request.Method.ToString();
result.Path = request.RequestUri?.AbsolutePath;
+ result.Version = request.Version;
foreach (var header in request.Headers)
{
// Extract method and path
requestData.Method = requestData.GetSingleHeaderValue(":method");
requestData.Path = requestData.GetSingleHeaderValue(":path");
+ requestData.Version = HttpVersion20.Value;
if (readBody && !endOfStream)
{
throw new NotImplementedException("HTTP/3 does not operate over a Socket.");
}
}
+
+ public static class HttpVersion30
+ {
+ public static readonly Version Value = new Version(3, 0);
+ }
}
break;
}
}
+ request.Version = HttpVersion30.Value;
return request;
}
_listenSocket = null;
}
}
+
public override async Task<GenericLoopbackConnection> EstablishGenericConnectionAsync()
{
Socket socket = await _listenSocket.AcceptAsync().ConfigureAwait(false);
Stream stream = new NetworkStream(socket, ownsSocket: true);
- if (_options.UseSsl)
+ var options = new GenericLoopbackOptions()
{
- var sslStream = new SslStream(stream, false, delegate { return true; });
-
- using (X509Certificate2 cert = Configuration.Certificates.GetServerCertificate())
- {
- SslServerAuthenticationOptions options = new SslServerAuthenticationOptions();
-
- options.EnabledSslProtocols = _options.SslProtocols;
-
- var protocols = new List<SslApplicationProtocol>();
- protocols.Add(SslApplicationProtocol.Http11);
- protocols.Add(SslApplicationProtocol.Http2);
- options.ApplicationProtocols = protocols;
+ Address = _options.Address,
+ SslProtocols = _options.SslProtocols,
+ UseSsl = false,
+ ListenBacklog = _options.ListenBacklog
+ };
- options.ServerCertificate = cert;
+ GenericLoopbackConnection connection = null;
- await sslStream.AuthenticateAsServerAsync(options, CancellationToken.None).ConfigureAwait(false);
+ try
+ {
+ if (_options.UseSsl)
+ {
+ var sslStream = new SslStream(stream, false, delegate { return true; });
+
+ using (X509Certificate2 cert = Configuration.Certificates.GetServerCertificate())
+ {
+ SslServerAuthenticationOptions sslOptions = new SslServerAuthenticationOptions();
+
+ sslOptions.EnabledSslProtocols = _options.SslProtocols;
+ sslOptions.ApplicationProtocols = _options.SslApplicationProtocols;
+ sslOptions.ServerCertificate = cert;
+
+ await sslStream.AuthenticateAsServerAsync(sslOptions, CancellationToken.None).ConfigureAwait(false);
+ }
+
+ stream = sslStream;
+ if (sslStream.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2)
+ {
+ // Do not pass original options so the CreateConnectionAsync won't try to do ALPN again.
+ return connection = await Http2LoopbackServerFactory.Singleton.CreateConnectionAsync(socket, stream, options).ConfigureAwait(false);
+ }
+ if (sslStream.NegotiatedApplicationProtocol == SslApplicationProtocol.Http11 ||
+ sslStream.NegotiatedApplicationProtocol == default)
+ {
+ // Do not pass original options so the CreateConnectionAsync won't try to do ALPN again.
+ return connection = await Http11LoopbackServerFactory.Singleton.CreateConnectionAsync(socket, stream, options).ConfigureAwait(false);
+ }
+ else
+ {
+ throw new Exception($"Unsupported negotiated protocol {sslStream.NegotiatedApplicationProtocol}");
+ }
}
- stream = sslStream;
- if (sslStream.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2)
+ if (_options.ClearTextVersion is null)
{
- // Do not pass original options so the CreateConnectionAsync won't try to do ALPN again.
- return await Http2LoopbackServerFactory.Singleton.CreateConnectionAsync(socket, stream);
+ throw new Exception($"HTTP server does not accept clear text connections, either set '{nameof(HttpAgnosticOptions.UseSsl)}' or set up '{nameof(HttpAgnosticOptions.ClearTextVersion)}' in server options.");
}
- if (sslStream.NegotiatedApplicationProtocol == SslApplicationProtocol.Http11)
+
+ var buffer = new byte[24];
+ var position = 0;
+ while (position < buffer.Length)
{
- // Do not pass original options so the CreateConnectionAsync won't try to do ALPN again.
- return await Http11LoopbackServerFactory.Singleton.CreateConnectionAsync(socket, stream);
+ var readBytes = await stream.ReadAsync(buffer, position, buffer.Length - position).ConfigureAwait(false);
+ if (readBytes == 0)
+ {
+ break;
+ }
+ position += readBytes;
}
- throw new Exception($"Unsupported negotiated protocol {sslStream.NegotiatedApplicationProtocol}");
- }
+
+ var memory = new Memory<byte>(buffer, 0, position);
+ stream = new ReturnBufferStream(stream, memory);
- var buffer = new byte[24];
- var position = 0;
- while (position < buffer.Length)
- {
- var readBytes = await stream.ReadAsync(buffer, position, buffer.Length - position).ConfigureAwait(false);
- if (readBytes == 0)
+ var prefix = Text.Encoding.ASCII.GetString(memory.Span);
+ if (prefix == Http2LoopbackConnection.Http2Prefix)
{
- break;
+ if (_options.ClearTextVersion == HttpVersion.Version20 || _options.ClearTextVersion == HttpVersion.Unknown)
+ {
+ return connection = await Http2LoopbackServerFactory.Singleton.CreateConnectionAsync(socket, stream, options).ConfigureAwait(false);
+ }
}
- position += readBytes;
- }
-
- var memory = new Memory<byte>(buffer, 0, position);
- stream = new ReturnBufferStream(stream, memory);
-
- var prefix = Text.Encoding.ASCII.GetString(memory.Span);
- if (prefix == Http2LoopbackConnection.Http2Prefix)
- {
- if (_options.ClearTextVersion == HttpVersion.Version20 || _options.ClearTextVersion == HttpVersion.Unknown)
+ else
{
- return await Http2LoopbackServerFactory.Singleton.CreateConnectionAsync(socket, stream);
+ if (_options.ClearTextVersion == HttpVersion.Version11 || _options.ClearTextVersion == HttpVersion.Unknown)
+ {
+ return connection = await Http11LoopbackServerFactory.Singleton.CreateConnectionAsync(socket, stream, options).ConfigureAwait(false);
+ }
}
+
+ throw new Exception($"HTTP/{_options.ClearTextVersion} server cannot establish connection due to unexpected data: '{prefix}'");
+ }
+ catch
+ {
+ connection?.Dispose();
+ connection = null;
+ stream.Dispose();
+ throw;
}
- else
+ finally
{
- if (_options.ClearTextVersion == HttpVersion.Version11 || _options.ClearTextVersion == HttpVersion.Unknown)
+ if (connection != null)
{
- return await Http11LoopbackServerFactory.Singleton.CreateConnectionAsync(socket, stream);
+ await connection.InitializeConnectionAsync().ConfigureAwait(false);
}
}
-
- throw new Exception($"HTTP/{_options.ClearTextVersion} server cannot establish connection due to unexpected data: '{prefix}'");
}
public override async Task<HttpRequestData> HandleRequestAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string content = "")
public class HttpAgnosticOptions : GenericLoopbackOptions
{
+ // Default null will raise an exception for any clear text protocol version
+ // Use HttpVersion.Unknown to use protocol version detection for clear text.
public Version ClearTextVersion { get; set; }
-
- public HttpAgnosticOptions()
- {
- ClearTextVersion = HttpVersion.Version11;
- }
+ public List<SslApplicationProtocol> SslApplicationProtocols { get; set; }
}
public sealed class HttpAgnosticLoopbackServerFactory : LoopbackServerFactory
public override void SetLength(long value) => _stream.SetLength(value);
public override void Write(byte[] buffer, int offset, int count) => _stream.Write(buffer, offset, count);
+ protected override void Dispose(bool disposing)
+ {
+ _stream.Dispose();
+ }
}
}
using HttpClient client = CreateHttpClient(handler);
var options = new GenericLoopbackOptions { Address = address };
+
await LoopbackServerFactory.CreateServerAsync(async (server, url) =>
{
_output.WriteLine(url.ToString());
string[] splits = Encoding.ASCII.GetString(headerLines[0]).Split(' ');
requestData.Method = splits[0];
requestData.Path = splits[1];
+ requestData.Version = Version.Parse(splits[2].Substring(splits[2].IndexOf('/') + 1));
// Convert header lines to key/value pairs
// Skip first line since it's the status line
.Where(a => a.IsIPv6LinkLocal)
.FirstOrDefault();
- public static void EnableUnencryptedHttp2IfNecessary(HttpClientHandler handler)
- {
- if (PlatformDetection.SupportsAlpn && !Capability.Http2ForceUnencryptedLoopback())
- {
- return;
- }
-
- FieldInfo socketsHttpHandlerField = typeof(HttpClientHandler).GetField("_underlyingHandler", BindingFlags.NonPublic | BindingFlags.Instance);
- if (socketsHttpHandlerField == null)
- {
- // Not using .NET Core implementation, i.e. could be .NET Framework.
- return;
- }
-
- object socketsHttpHandler = socketsHttpHandlerField.GetValue(handler);
- Assert.NotNull(socketsHttpHandler);
-
- EnableUncryptedHttp2(socketsHttpHandler);
- }
-
-#if !NETFRAMEWORK
- public static void EnableUnencryptedHttp2IfNecessary(SocketsHttpHandler socketsHttpHandler)
- {
- if (PlatformDetection.SupportsAlpn && !Capability.Http2ForceUnencryptedLoopback())
- {
- return;
- }
-
- EnableUncryptedHttp2(socketsHttpHandler);
- }
-#endif
-
- private static void EnableUncryptedHttp2(object socketsHttpHandler)
- {
- // Get HttpConnectionSettings object from SocketsHttpHandler.
- Type socketsHttpHandlerType = typeof(HttpClientHandler).Assembly.GetType("System.Net.Http.SocketsHttpHandler");
- FieldInfo settingsField = socketsHttpHandlerType.GetField("_settings", BindingFlags.NonPublic | BindingFlags.Instance);
- Assert.NotNull(settingsField);
- object settings = settingsField.GetValue(socketsHttpHandler);
- Assert.NotNull(settings);
-
- // Allow HTTP/2.0 via unencrypted socket if ALPN is not supported on platform.
- Type httpConnectionSettingsType = typeof(HttpClientHandler).Assembly.GetType("System.Net.Http.HttpConnectionSettings");
- FieldInfo allowUnencryptedHttp2Field = httpConnectionSettingsType.GetField("_allowUnencryptedHttp2", BindingFlags.NonPublic | BindingFlags.Instance);
- Assert.NotNull(allowUnencryptedHttp2Field);
- allowUnencryptedHttp2Field.SetValue(settings, true);
- }
-
public static byte[] GenerateRandomContent(int size)
{
byte[] data = new byte[size];
public static System.Net.IWebProxy DefaultProxy { get { throw null; } set { } }
public System.Net.Http.Headers.HttpRequestHeaders DefaultRequestHeaders { get { throw null; } }
public System.Version DefaultRequestVersion { get { throw null; } set { } }
+ public System.Net.Http.HttpVersionPolicy DefaultVersionPolicy { get { throw null; } set { } }
public long MaxResponseContentBufferSize { get { throw null; } set { } }
public System.TimeSpan Timeout { get { throw null; } set { } }
public void CancelPendingRequests() { }
public HttpRequestOptions Options { get { throw null; } }
public System.Uri? RequestUri { get { throw null; } set { } }
public System.Version Version { get { throw null; } set { } }
+ public System.Net.Http.HttpVersionPolicy VersionPolicy { get { throw null; } set { } }
public void Dispose() { }
protected virtual void Dispose(bool disposing) { }
public override string ToString() { throw null; }
public System.Net.Http.HttpResponseMessage EnsureSuccessStatusCode() { throw null; }
public override string ToString() { throw null; }
}
+ public enum HttpVersionPolicy
+ {
+ RequestVersionOrLower = 0,
+ RequestVersionOrHigher = 1,
+ RequestVersionExact = 2,
+ }
public abstract partial class MessageProcessingHandler : System.Net.Http.DelegatingHandler
{
protected MessageProcessingHandler() { }
<data name="net_http_http2_sync_not_supported" xml:space="preserve">
<value>The synchronous method is not supported by '{0}' for HTTP/2 or higher. Either use an asynchronous method or downgrade the request version to HTTP/1.1 or lower.</value>
</data>
+ <data name="net_http_upgrade_not_enabled_sync" xml:space="preserve">
+ <value>HTTP request version upgrade is not enabled for synchronous '{0}'. Do not use '{1}' version policy for synchronous HTTP methods.</value>
+ </data>
+ <data name="net_http_requested_version_not_enabled" xml:space="preserve">
+ <value>Requesting HTTP version {0} with version policy {1} while HTTP/{2} is not enabled.</value>
+ </data>
+ <data name="net_http_requested_version_cannot_establish" xml:space="preserve">
+ <value>Requesting HTTP version {0} with version policy {1} while unable to establish HTTP/{2} connection.</value>
+ </data>
+ <data name="net_http_requested_version_alpn_refused" xml:space="preserve">
+ <value>Requesting HTTP version {0} with version policy {1} while server returned HTTP/1.1 in ALPN.</value>
+ </data>
+ <data name="net_http_requested_version_server_refused" xml:space="preserve">
+ <value>Requesting HTTP version {0} with version policy {1} while server offers only version fallback.</value>
+ </data>
</root>
<Compile Include="System\Net\Http\HttpRuleParser.cs" />
<Compile Include="System\Net\Http\HttpTelemetry.cs" />
<Compile Include="System\Net\Http\HttpUtilities.cs" />
+ <Compile Include="System\Net\Http\HttpVersionPolicy.cs" />
<Compile Include="System\Net\Http\MessageProcessingHandler.cs" />
<Compile Include="System\Net\Http\MultipartContent.cs" />
<Compile Include="System\Net\Http\MultipartFormDataContent.cs" />
private CancellationTokenSource _pendingRequestsCts;
private HttpRequestHeaders? _defaultRequestHeaders;
private Version _defaultRequestVersion = HttpUtilities.DefaultRequestVersion;
+ private HttpVersionPolicy _defaultVersionPolicy = HttpUtilities.DefaultVersionPolicy;
private Uri? _baseAddress;
private TimeSpan _timeout;
}
}
+ /// <summary>
+ /// Gets or sets the default value of <see cref="HttpRequestMessage.VersionPolicy" /> for implicitly created requests in convenience methods,
+ /// e.g.: <see cref="GetAsync(string?)" />, <see cref="PostAsync(string?, HttpContent)" />.
+ /// </summary>
+ /// <remarks>
+ /// Note that this property has no effect on any of the <see cref="Send(HttpRequestMessage)" /> and <see cref="SendAsync(HttpRequestMessage)" /> overloads
+ /// since they accept fully initialized <see cref="HttpRequestMessage" />.
+ /// </remarks>
+ public HttpVersionPolicy DefaultVersionPolicy
+ {
+ get => _defaultVersionPolicy;
+ set
+ {
+ CheckDisposedOrStarted();
+ _defaultVersionPolicy = value;
+ }
+ }
+
public Uri? BaseAddress
{
get { return _baseAddress; }
string.IsNullOrEmpty(uri) ? null : new Uri(uri, UriKind.RelativeOrAbsolute);
private HttpRequestMessage CreateRequestMessage(HttpMethod method, Uri? uri) =>
- new HttpRequestMessage(method, uri) { Version = _defaultRequestVersion };
+ new HttpRequestMessage(method, uri) { Version = _defaultRequestVersion, VersionPolicy = _defaultVersionPolicy };
#endregion Private Helpers
}
}
private Uri? _requestUri;
private HttpRequestHeaders? _headers;
private Version _version;
+ private HttpVersionPolicy _versionPolicy;
private HttpContent? _content;
private bool _disposed;
private HttpRequestOptions? _options;
}
}
+ /// <summary>
+ /// Gets or sets the policy determining how <see cref="Version" /> is interpreted and how is the final HTTP version negotiated with the server.
+ /// </summary>
+ public HttpVersionPolicy VersionPolicy
+ {
+ get { return _versionPolicy; }
+ set
+ {
+ CheckDisposed();
+
+ _versionPolicy = value;
+ }
+ }
+
public HttpContent? Content
{
get { return _content; }
_method = method;
_requestUri = requestUri;
_version = HttpUtilities.DefaultRequestVersion;
+ _versionPolicy = HttpUtilities.DefaultVersionPolicy;
}
internal bool MarkAsSent()
internal static Version DefaultResponseVersion => HttpVersion.Version11;
+ internal static HttpVersionPolicy DefaultVersionPolicy => HttpVersionPolicy.RequestVersionOrLower;
+
internal static bool IsHttpUri(Uri uri)
{
Debug.Assert(uri != null);
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Net.Http
+{
+ /// <summary>
+ /// Determines behavior when selecting and negotiating HTTP version for a request.
+ /// </summary>
+ public enum HttpVersionPolicy
+ {
+ /// <summary>
+ /// Default behavior, either uses requested version or downgrades to a lower one.
+ /// </summary>
+ /// <remarks>
+ /// If the server supports the requested version, either negotiated via ALPN (H2) or advertised via Alt-Svc (H3),
+ /// as well as a secure connection is being requested, the result is the <see cref="HttpRequestMessage.Version" />.
+ /// Otherwise, downgrades to HTTP/1.1.
+ /// Note that this option does not allow use of prenegotiated clear text connection, e.g. H2C.
+ /// </remarks>
+ RequestVersionOrLower,
+
+ /// <summary>
+ /// Tries to uses highest available version, downgrading only to the requested version, not bellow.
+ /// Throwing <see cref="HttpRequestException" /> if a connection with higher or equal version cannot be established.
+ /// </summary>
+ /// <remarks>
+ /// If the server supports higher than requested version, either negotiated via ALPN (H2) or advertised via Alt-Svc (H3),
+ /// as well as secure connection is being requested, the result is the highest available one.
+ /// Otherwise, downgrades to the <see cref="HttpRequestMessage.Version" />.
+ /// Note that this option allows to use prenegotiated clear text connection for the requested version but not for anything higher.
+ /// </remarks>
+ RequestVersionOrHigher,
+
+ /// <summary>
+ /// Uses only the requested version.
+ /// Throwing <see cref="HttpRequestException" /> if a connection with the exact version cannot be established.
+ /// </summary>
+ /// <remarks>
+ /// Note that this option allows to use prenegotiated clear text connection for the requested version.
+ /// </remarks>
+ RequestVersionExact
+ }
+}
return expired;
}
- internal static HttpRequestException CreateRetryException()
- {
- // This is an exception that's thrown during request processing to indicate that the
- // attempt to send the request failed in such a manner that the server is guaranteed to not have
- // processed the request in any way, and thus the request can be retried.
- // This will be caught in HttpConnectionPool.SendWithRetryAsync and the retry logic will kick in.
- // The user should never see this exception.
- throw new HttpRequestException(null, null, allowRetry: RequestRetryType.RetryOnSameOrNextProxy);
- }
-
internal static bool IsDigit(byte c) => (uint)(c - '0') <= '9' - '0';
internal static int ParseStatusCode(ReadOnlySpan<byte> value)
/// <summary>Options specialized and cached for this pool and its key.</summary>
private readonly SslClientAuthenticationOptions? _sslOptionsHttp11;
private readonly SslClientAuthenticationOptions? _sslOptionsHttp2;
+ private readonly SslClientAuthenticationOptions? _sslOptionsHttp2Only;
private readonly SslClientAuthenticationOptions? _sslOptionsHttp3;
/// <summary>Queue of waiters waiting for a connection. Created on demand.</summary>
if (host != null)
{
_originAuthority = new HttpAuthority(host, port);
-
- if (_poolManager.Settings._assumePrenegotiatedHttp3ForTesting)
- {
- _http3Authority = _originAuthority;
- }
}
_http2Enabled = _poolManager.Settings._maxHttpVersion >= HttpVersion.Version20;
Debug.Assert(port != 0);
Debug.Assert(sslHostName == null);
Debug.Assert(proxyUri == null);
- _http2Enabled = _poolManager.Settings._allowUnencryptedHttp2;
+
_http3Enabled = false;
break;
Debug.Assert(port != 0);
Debug.Assert(sslHostName != null);
Debug.Assert(proxyUri != null);
+
_http3Enabled = false; // TODO: how do we tunnel HTTP3?
break;
{
_sslOptionsHttp2 = ConstructSslOptions(poolManager, sslHostName);
_sslOptionsHttp2.ApplicationProtocols = s_http2ApplicationProtocols;
+ _sslOptionsHttp2Only = ConstructSslOptions(poolManager, sslHostName);
+ _sslOptionsHttp2Only.ApplicationProtocols = s_http2OnlyApplicationProtocols;
// Note:
// The HTTP/2 specification states:
Debug.Assert(hostHeader != null);
_http2EncodedAuthorityHostHeader = HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(H2StaticTable.Authority, hostHeader);
_http3EncodedAuthorityHostHeader = QPackEncoder.EncodeLiteralHeaderFieldWithStaticNameReferenceToArray(H3StaticTable.Authority, hostHeader);
-
}
if (_http3Enabled)
private static readonly List<SslApplicationProtocol> s_http3ApplicationProtocols = new List<SslApplicationProtocol>() { Http3Connection.Http3ApplicationProtocol };
private static readonly List<SslApplicationProtocol> s_http2ApplicationProtocols = new List<SslApplicationProtocol>() { SslApplicationProtocol.Http2, SslApplicationProtocol.Http11 };
+ private static readonly List<SslApplicationProtocol> s_http2OnlyApplicationProtocols = new List<SslApplicationProtocol>() { SslApplicationProtocol.Http2 };
private static SslClientAuthenticationOptions ConstructSslOptions(HttpConnectionPoolManager poolManager, string sslHostName)
{
public HttpAuthority? OriginAuthority => _originAuthority;
public HttpConnectionSettings Settings => _poolManager.Settings;
- public bool IsSecure => _sslOptionsHttp11 != null;
public HttpConnectionKind Kind => _kind;
+ public bool IsSecure => _kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel;
public bool AnyProxyKind => (_proxyUri != null);
public Uri? ProxyUri => _proxyUri;
public ICredentials? ProxyCredentials => _poolManager.ProxyCredentials;
private ValueTask<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)>
GetConnectionAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken)
{
- if (_http3Enabled && request.Version.Major >= 3)
+ // Do not even attempt at getting/creating a connection if it's already obvious we cannot provided the one requested.
+ if (request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
+ {
+ if (request.Version.Major == 3 && !_http3Enabled)
+ {
+ return ValueTask.FromException<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)>(
+ new HttpRequestException(SR.Format(SR.net_http_requested_version_not_enabled, request.Version, request.VersionPolicy, 3)));
+ }
+ if (request.Version.Major == 2 && !_http2Enabled)
+ {
+ return ValueTask.FromException<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)>(
+ new HttpRequestException(SR.Format(SR.net_http_requested_version_not_enabled, request.Version, request.VersionPolicy, 2)));
+ }
+ }
+
+ // Either H3 explicitly requested or secured upgraded allowed.
+ if (_http3Enabled && (request.Version.Major >= 3 || (request.VersionPolicy == HttpVersionPolicy.RequestVersionOrHigher && IsSecure)))
{
HttpAuthority? authority = _http3Authority;
+ // H3 is explicitly requested, assume prenegotiated H3.
+ if (request.Version.Major >= 3 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
+ {
+ authority = authority ?? _originAuthority;
+ }
if (authority != null)
{
+ if (IsAltSvcBlocked(authority))
+ {
+ return ValueTask.FromException<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)>(
+ new HttpRequestException(SR.Format(SR.net_http_requested_version_cannot_establish, request.Version, request.VersionPolicy, 3)));
+ }
+
return GetHttp3ConnectionAsync(request, authority, cancellationToken);
}
}
+ // If we got here, we cannot provide HTTP/3 connection. Do not continue if downgrade is not allowed.
+ if (request.Version.Major >= 3 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
+ {
+ return ValueTask.FromException<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)>(
+ new HttpRequestException(SR.Format(SR.net_http_requested_version_cannot_establish, request.Version, request.VersionPolicy, 3)));
+ }
- if (_http2Enabled && request.Version.Major >= 2)
+ if (_http2Enabled && (request.Version.Major >= 2 || (request.VersionPolicy == HttpVersionPolicy.RequestVersionOrHigher && IsSecure)) &&
+ // If the connection is not secured and downgrade is possible, prefer HTTP/1.1.
+ (request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower || IsSecure))
{
return GetHttp2ConnectionAsync(request, async, cancellationToken);
}
+ // If we got here, we cannot provide HTTP/2 connection. Do not continue if downgrade is not allowed.
+ if (request.Version.Major >= 2 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
+ {
+ return ValueTask.FromException<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)>(
+ new HttpRequestException(SR.Format(SR.net_http_requested_version_cannot_establish, request.Version, request.VersionPolicy, 2)));
+ }
return GetHttpConnectionAsync(request, async, cancellationToken);
}
HttpResponseMessage? failureResponse;
(connection, transportContext, failureResponse) =
- await ConnectAsync(request, async, true, cancellationToken).ConfigureAwait(false);
+ await ConnectAsync(request, async, cancellationToken).ConfigureAwait(false);
if (failureResponse != null)
{
{
_http2Enabled = false;
+ if (request.Version.Major >= 2 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
+ {
+ sslStream.Close();
+ throw new HttpRequestException(SR.Format(SR.net_http_requested_version_server_refused, request.Version, request.VersionPolicy));
+ }
+
if (_associatedConnectionCount < _maxConnections)
{
IncrementConnectionCountNoLock();
Trace("Attempting new HTTP3 connection.");
}
- QuicConnection quicConnection = await ConnectHelper.ConnectQuicAsync(authority.IdnHost, authority.Port, _sslOptionsHttp3, cancellationToken).ConfigureAwait(false);
+ QuicConnection quicConnection;
+ try
+ {
+ quicConnection = await ConnectHelper.ConnectQuicAsync(authority.IdnHost, authority.Port, _sslOptionsHttp3, cancellationToken).ConfigureAwait(false);
+ }
+ catch
+ {
+ // Disables HTTP/3 until server announces it can handle it via Alt-Svc.
+ BlocklistAuthority(authority);
+ throw;
+ }
//TODO: NegotiatedApplicationProtocol not yet implemented.
#if false
}
catch (HttpRequestException e) when (e.AllowRetry == RequestRetryType.RetryOnLowerHttpVersion)
{
+ // Throw since fallback is not allowed by the version policy.
+ if (request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
+ {
+ throw new HttpRequestException(SR.Format(SR.net_http_requested_version_server_refused, request.Version, request.VersionPolicy), e);
+ }
+
if (NetEventSource.Log.IsEnabled())
{
Trace($"Retrying request after exception on existing connection: {e}");
}
// Eat exception and try again on a lower protocol version.
-
Debug.Assert(connection is HttpConnection == false, $"{nameof(RequestRetryType.RetryOnLowerHttpVersion)} should not be thrown by HTTP/1 connections.");
request.Version = HttpVersion.Version11;
continue;
{
var authority = new HttpAuthority(value.Host!, value.Port);
- if (_altSvcBlocklist != null)
+ if (IsAltSvcBlocked(authority))
{
- lock (_altSvcBlocklist)
- {
- if (_altSvcBlocklist.Contains(authority))
- {
- // Skip authorities in our blocklist.
- continue;
- }
- }
+ // Skip authorities in our blocklist.
+ continue;
}
TimeSpan authorityMaxAge = value.MaxAge;
}
/// <summary>
+ /// Checks whether the given <paramref name="authority"/> is on the currext Alt-Svc blocklist.
+ /// </summary>
+ /// <seealso cref="BlocklistAuthority" />
+ private bool IsAltSvcBlocked(HttpAuthority authority)
+ {
+ if (_altSvcBlocklist != null)
+ {
+ lock (_altSvcBlocklist)
+ {
+ return _altSvcBlocklist.Contains(authority);
+ }
+ }
+ return false;
+ }
+
+ /// <summary>
/// Blocklists an authority and resets the current authority back to origin.
/// If the number of blocklisted authorities exceeds <see cref="MaxAltSvcIgnoreListSize"/>,
/// Alt-Svc will be disabled entirely for a period of time.
internal void BlocklistAuthority(HttpAuthority badAuthority)
{
Debug.Assert(badAuthority != null);
- Debug.Assert(badAuthority != _originAuthority);
HashSet<HttpAuthority>? altSvcBlocklist = _altSvcBlocklist;
return SendWithProxyAuthAsync(request, async, doRequestAuth, cancellationToken);
}
- private async ValueTask<(Connection?, TransportContext?, HttpResponseMessage?)> ConnectAsync(HttpRequestMessage request, bool async, bool allowHttp2, CancellationToken cancellationToken)
+ private async ValueTask<(Connection?, TransportContext?, HttpResponseMessage?)> ConnectAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken)
{
// If a non-infinite connect timeout has been set, create and use a new CancellationToken that will be canceled
// when either the original token is canceled or a connect timeout occurs.
Debug.Assert(connection != null);
TransportContext? transportContext = null;
- if (_kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel)
+ if (IsSecure)
{
- SslStream sslStream = await ConnectHelper.EstablishSslConnectionAsync(allowHttp2 ? _sslOptionsHttp2! : _sslOptionsHttp11!, request, async, connection.Stream, cancellationToken).ConfigureAwait(false);
+ SslStream sslStream = await ConnectHelper.EstablishSslConnectionAsync(GetSslOptionsForRequest(request), request, async, connection.Stream, cancellationToken).ConfigureAwait(false);
connection = Connection.FromStream(sslStream, leaveOpen: false, connection.ConnectionProperties, connection.LocalEndPoint, connection.RemoteEndPoint);
transportContext = sslStream.TransportContext;
}
internal async ValueTask<(HttpConnection?, HttpResponseMessage?)> CreateHttp11ConnectionAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken)
{
(Connection? connection, TransportContext? transportContext, HttpResponseMessage? failureResponse) =
- await ConnectAsync(request, async, false, cancellationToken).ConfigureAwait(false);
+ await ConnectAsync(request, async, cancellationToken).ConfigureAwait(false);
if (failureResponse != null)
{
return (ConstructHttp11Connection(connection!, transportContext), null);
}
+ private SslClientAuthenticationOptions GetSslOptionsForRequest(HttpRequestMessage request)
+ {
+ if (_http2Enabled)
+ {
+ if (request.Version.Major >= 2 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
+ {
+ return _sslOptionsHttp2Only!;
+ }
+
+ if (request.Version.Major >= 2 || request.VersionPolicy == HttpVersionPolicy.RequestVersionOrHigher)
+ {
+ return _sslOptionsHttp2!;
+ }
+ }
+ return _sslOptionsHttp11!;
+ }
+
private HttpConnection ConstructHttp11Connection(Connection connection, TransportContext? transportContext)
{
return new HttpConnection(this, connection, transportContext);
}
else
{
- // No explicit Host header. Use host from uri.
+ // No explicit Host header. Use host from uri.
sslHostName = uri.IdnHost;
}
}
{
private const string Http2SupportEnvironmentVariableSettingName = "DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP2SUPPORT";
private const string Http2SupportAppCtxSettingName = "System.Net.Http.SocketsHttpHandler.Http2Support";
- private const string Http2UnencryptedSupportEnvironmentVariableSettingName = "DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP2UNENCRYPTEDSUPPORT";
- private const string Http2UnencryptedSupportAppCtxSettingName = "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport";
private const string Http3DraftSupportEnvironmentVariableSettingName = "DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP3DRAFTSUPPORT";
private const string Http3DraftSupportAppCtxSettingName = "System.Net.SocketsHttpHandler.Http3DraftSupport";
internal Version _maxHttpVersion;
- internal bool _allowUnencryptedHttp2;
-
- // Used for testing until https://github.com/dotnet/runtime/issues/987
- internal bool _assumePrenegotiatedHttp3ForTesting;
-
internal SslClientAuthenticationOptions? _sslOptions;
internal bool _enableMultipleHttp2Connections;
AllowDraftHttp3 && allowHttp2 ? Http3Connection.HttpVersion30 :
allowHttp2 ? HttpVersion.Version20 :
HttpVersion.Version11;
- _allowUnencryptedHttp2 = allowHttp2 && AllowUnencryptedHttp2;
_defaultCredentialsUsedForProxy = _proxy != null && (_proxy.Credentials == CredentialCache.DefaultCredentials || _defaultProxyCredentials == CredentialCache.DefaultCredentials);
_defaultCredentialsUsedForServer = _credentials == CredentialCache.DefaultCredentials;
}
_sslOptions = _sslOptions?.ShallowClone(), // shallow clone the options for basic prevention of mutation issues while processing
_useCookies = _useCookies,
_useProxy = _useProxy,
- _allowUnencryptedHttp2 = _allowUnencryptedHttp2,
- _assumePrenegotiatedHttp3ForTesting = _assumePrenegotiatedHttp3ForTesting,
_requestHeaderEncodingSelector = _requestHeaderEncodingSelector,
_responseHeaderEncodingSelector = _responseHeaderEncodingSelector,
_enableMultipleHttp2Connections = _enableMultipleHttp2Connections,
}
}
- private static bool AllowUnencryptedHttp2
- {
- get
- {
- // Default to not allowing unencrypted HTTP/2, but enable that to be overridden
- // by an AppContext switch, or by an environment variable being to to true/1.
-
- // First check for the AppContext switch, giving it priority over the environment variable.
- if (AppContext.TryGetSwitch(Http2UnencryptedSupportAppCtxSettingName, out bool allowHttp2))
- {
- return allowHttp2;
- }
-
- // AppContext switch wasn't used. Check the environment variable.
- string? envVar = Environment.GetEnvironmentVariable(Http2UnencryptedSupportEnvironmentVariableSettingName);
- if (envVar != null && (envVar.Equals("true", StringComparison.OrdinalIgnoreCase) || envVar.Equals("1")))
- {
- // Allow HTTP/2.0 protocol for HTTP endpoints.
- return true;
- }
-
- // Default to a maximum of HTTP/1.1.
- return false;
- }
- }
-
private static bool AllowDraftHttp3
{
get
throw new NotSupportedException(SR.Format(SR.net_http_http2_sync_not_supported, GetType()));
}
+ // Do not allow upgrades for synchronous requests, that might lead to asynchronous code-paths.
+ if (request.VersionPolicy == HttpVersionPolicy.RequestVersionOrHigher)
+ {
+ throw new NotSupportedException(SR.Format(SR.net_http_upgrade_not_enabled_sync, nameof(Send), request.VersionPolicy));
+ }
+
CheckDisposed();
HttpMessageHandlerStage handler = _handler ?? SetupHandlerChain();
/// </summary>
protected override HttpClient CreateHttpClient()
{
- bool http3Enabled = (bool)typeof(SocketsHttpHandler)
- .Assembly
- .GetType("System.Net.Http.HttpConnectionSettings", throwOnError: true)
- .GetProperty("AllowDraftHttp3", Reflection.BindingFlags.Static | Reflection.BindingFlags.NonPublic)
- .GetValue(null);
-
- Assert.True(http3Enabled, "HTTP/3 draft support must be enabled for this test.");
-
HttpClientHandler handler = CreateHttpClientHandler(HttpVersion30);
- SetUsePrenegotiatedHttp3(handler, usePrenegotiatedHttp3: false);
return CreateHttpClient(handler);
}
using (var handler = new SocketsHttpHandler())
using (HttpClient client = CreateHttpClient(handler))
{
- TestHelper.EnableUnencryptedHttp2IfNecessary(handler);
handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };
// Increase default Expect: 100-continue timeout to ensure that we don't accidentally fire the timer and send the request body.
handler.Expect100ContinueTimeout = TimeSpan.FromSeconds(300);
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Version = new Version(2,0);
+ request.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
request.Content = new StringContent(new string('*', 3000));
request.Headers.ExpectContinue = true;
request.Headers.Add("x-test", "PostAsyncExpect100Continue_NonSuccessResponse_RequestBodyNotSent");
using System.IO;
using System.Reflection;
+using System.Threading.Tasks;
namespace System.Net.Http.Functional.Tests
{
{
useVersion ??= HttpVersion.Version11;
- HttpClientHandler handler = new HttpClientHandler();
+ HttpClientHandler handler = PlatformDetection.SupportsAlpn ? new HttpClientHandler() : new VersionHttpClientHandler(useVersion);
if (useVersion >= HttpVersion.Version20)
{
- TestHelper.EnableUnencryptedHttp2IfNecessary(handler);
handler.ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates;
}
+ return handler;
+ }
- if (useVersion == HttpVersion30)
+ protected static object GetUnderlyingSocketsHttpHandler(HttpClientHandler handler)
+ {
+ FieldInfo field = typeof(HttpClientHandler).GetField("_underlyingHandler", BindingFlags.Instance | BindingFlags.NonPublic);
+ return field?.GetValue(handler);
+ }
+
+ protected static HttpRequestMessage CreateRequest(HttpMethod method, Uri uri, Version version, bool exactVersion = false) =>
+ new HttpRequestMessage(method, uri)
{
- SetUsePrenegotiatedHttp3(handler, usePrenegotiatedHttp3: true);
- }
+ Version = version,
+ VersionPolicy = exactVersion ? HttpVersionPolicy.RequestVersionExact : HttpVersionPolicy.RequestVersionOrLower
+ };
+ }
- return handler;
+ internal class VersionHttpClientHandler : HttpClientHandler
+ {
+ private readonly Version _useVersion;
+
+ public VersionHttpClientHandler(Version useVersion)
+ {
+ _useVersion = useVersion;
}
- /// <summary>
- /// Used to bypass Alt-Svc until https://github.com/dotnet/runtime/issues/987
- /// </summary>
- protected static void SetUsePrenegotiatedHttp3(HttpClientHandler handler, bool usePrenegotiatedHttp3)
+ protected override HttpResponseMessage Send(HttpRequestMessage request, Threading.CancellationToken cancellationToken)
{
- object socketsHttpHandler = GetUnderlyingSocketsHttpHandler(handler);
- object settings = socketsHttpHandler.GetType().GetField("_settings", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(socketsHttpHandler);
- settings.GetType().GetField("_assumePrenegotiatedHttp3ForTesting", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(settings, usePrenegotiatedHttp3);
+ if (request.Version == _useVersion)
+ {
+ request.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
+ }
+
+ return base.Send(request, cancellationToken);
}
- protected static object GetUnderlyingSocketsHttpHandler(HttpClientHandler handler)
+ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, Threading.CancellationToken cancellationToken)
{
- FieldInfo field = typeof(HttpClientHandler).GetField("_underlyingHandler", BindingFlags.Instance | BindingFlags.NonPublic);
- return field?.GetValue(handler);
+
+ if (request.Version == _useVersion)
+ {
+ request.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
+ }
+
+ return base.SendAsync(request, cancellationToken);
}
protected static HttpRequestMessage CreateRequest(HttpMethod method, Uri uri, Version version, bool exactVersion = false) =>
using System.Diagnostics;
using System.IO;
using System.Linq;
+using System.Net.Quic;
+using System.Net.Security;
using System.Net.Sockets;
using System.Net.Test.Common;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
+using static System.Net.Test.Common.Configuration.Http;
namespace System.Net.Http.Functional.Tests
{
Assert.IsType<TimeoutException>(ex.InnerException);
},
async server =>
- {
+ {
await server.AcceptConnectionAsync(async connection =>
{
try
});
}
+ public static IEnumerable<object[]> VersionSelectionMemberData()
+ {
+ var serverOptions = new GenericLoopbackOptions();
+ // Either we support SSL (ALPN), or we're testing only clear text.
+ foreach (var useSsl in BoolValues.Where(b => serverOptions.UseSsl || !b))
+ {
+ yield return new object[] { HttpVersion.Version11, HttpVersionPolicy.RequestVersionOrLower, HttpVersion.Version11, useSsl, HttpVersion.Version11 };
+ yield return new object[] { HttpVersion.Version11, HttpVersionPolicy.RequestVersionExact, HttpVersion.Version11, useSsl, HttpVersion.Version11 };
+ yield return new object[] { HttpVersion.Version11, HttpVersionPolicy.RequestVersionOrHigher, HttpVersion.Version11, useSsl, HttpVersion.Version11 };
+ yield return new object[] { HttpVersion.Version11, HttpVersionPolicy.RequestVersionOrLower, HttpVersion.Version20, useSsl, useSsl ? (object)HttpVersion.Version11 : typeof(HttpRequestException) };
+ yield return new object[] { HttpVersion.Version11, HttpVersionPolicy.RequestVersionExact, HttpVersion.Version20, useSsl, useSsl ? (object)HttpVersion.Version11 : typeof(HttpRequestException) };
+ yield return new object[] { HttpVersion.Version11, HttpVersionPolicy.RequestVersionOrHigher, HttpVersion.Version20, useSsl, useSsl ? (object)HttpVersion.Version20 : typeof(HttpRequestException) };
+ if (QuicConnection.IsQuicSupported)
+ {
+ yield return new object[] { HttpVersion.Version11, HttpVersionPolicy.RequestVersionOrLower, HttpVersion30, useSsl, HttpVersion.Version11 };
+ yield return new object[] { HttpVersion.Version11, HttpVersionPolicy.RequestVersionExact, HttpVersion30, useSsl, HttpVersion.Version11 };
+ yield return new object[] { HttpVersion.Version11, HttpVersionPolicy.RequestVersionOrHigher, HttpVersion30, useSsl, useSsl ? HttpVersion30 : HttpVersion.Version11 };
+ }
+
+ yield return new object[] { HttpVersion.Version20, HttpVersionPolicy.RequestVersionOrLower, HttpVersion.Version11, useSsl, HttpVersion.Version11 };
+ yield return new object[] { HttpVersion.Version20, HttpVersionPolicy.RequestVersionExact, HttpVersion.Version11, useSsl, typeof(HttpRequestException) };
+ yield return new object[] { HttpVersion.Version20, HttpVersionPolicy.RequestVersionOrHigher, HttpVersion.Version11, useSsl, typeof(HttpRequestException) };
+ yield return new object[] { HttpVersion.Version20, HttpVersionPolicy.RequestVersionOrLower, HttpVersion.Version20, useSsl, useSsl ? (object)HttpVersion.Version20 : typeof(HttpRequestException) };
+ yield return new object[] { HttpVersion.Version20, HttpVersionPolicy.RequestVersionExact, HttpVersion.Version20, useSsl, HttpVersion.Version20 };
+ yield return new object[] { HttpVersion.Version20, HttpVersionPolicy.RequestVersionOrHigher, HttpVersion.Version20, useSsl, HttpVersion.Version20 };
+ if (QuicConnection.IsQuicSupported)
+ {
+ yield return new object[] { HttpVersion.Version20, HttpVersionPolicy.RequestVersionOrLower, HttpVersion30, useSsl, useSsl ? HttpVersion.Version20 : HttpVersion.Version11 };
+ yield return new object[] { HttpVersion.Version20, HttpVersionPolicy.RequestVersionExact, HttpVersion30, useSsl, HttpVersion.Version20 };
+ yield return new object[] { HttpVersion.Version20, HttpVersionPolicy.RequestVersionOrHigher, HttpVersion30, useSsl, useSsl ? (object)HttpVersion30 : typeof(HttpRequestException) };
+ }
+
+ if (QuicConnection.IsQuicSupported)
+ {
+ yield return new object[] { HttpVersion30, HttpVersionPolicy.RequestVersionOrLower, HttpVersion.Version11, useSsl, useSsl ? HttpVersion30 : HttpVersion.Version11 };
+ yield return new object[] { HttpVersion30, HttpVersionPolicy.RequestVersionExact, HttpVersion.Version11, useSsl, typeof(HttpRequestException) };
+ yield return new object[] { HttpVersion30, HttpVersionPolicy.RequestVersionOrHigher, HttpVersion.Version11, useSsl, typeof(HttpRequestException) };
+ yield return new object[] { HttpVersion30, HttpVersionPolicy.RequestVersionOrLower, HttpVersion.Version20, useSsl, useSsl ? HttpVersion30 : HttpVersion.Version11 };
+ yield return new object[] { HttpVersion30, HttpVersionPolicy.RequestVersionExact, HttpVersion.Version20, useSsl, typeof(HttpRequestException) };
+ yield return new object[] { HttpVersion30, HttpVersionPolicy.RequestVersionOrHigher, HttpVersion.Version20, useSsl, typeof(HttpRequestException) };
+ yield return new object[] { HttpVersion30, HttpVersionPolicy.RequestVersionOrLower, HttpVersion30, useSsl, useSsl ? HttpVersion30 : HttpVersion.Version11 };
+ yield return new object[] { HttpVersion30, HttpVersionPolicy.RequestVersionExact, HttpVersion30, useSsl, useSsl ? (object)HttpVersion30 : typeof(HttpRequestException) };
+ yield return new object[] { HttpVersion30, HttpVersionPolicy.RequestVersionOrHigher, HttpVersion30, useSsl, useSsl ? (object)HttpVersion30 : typeof(HttpRequestException) };
+ }
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(VersionSelectionMemberData))]
+ public async Task SendAsync_CorrectVersionSelected_LoopbackServer(Version requestVersion, HttpVersionPolicy versionPolicy, Version serverVersion, bool useSsl, object expectedResult)
+ {
+ await HttpAgnosticLoopbackServer.CreateClientAndServerAsync(
+ async uri =>
+ {
+ var request = new HttpRequestMessage(HttpMethod.Get, uri)
+ {
+ Version = requestVersion,
+ VersionPolicy = versionPolicy
+ };
+
+ using HttpClientHandler handler = CreateHttpClientHandler();
+ if (useSsl)
+ {
+ handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
+ }
+ using HttpClient client = CreateHttpClient(handler);
+ if (expectedResult is Type type)
+ {
+ Exception exception = await Assert.ThrowsAnyAsync<Exception>(() => client.SendAsync(request));
+ Assert.IsType(type, exception);
+ _output.WriteLine("Client expected exception: " + exception.ToString());
+ }
+ else
+ {
+ HttpResponseMessage response = await client.SendAsync(request);
+ Assert.Equal(expectedResult, response.Version);
+ }
+ },
+ async server =>
+ {
+ try
+ {
+ HttpRequestData requestData = await server.AcceptConnectionSendResponseAndCloseAsync();
+ Assert.Equal(expectedResult, requestData.Version);
+ }
+ catch (Exception ex) when (expectedResult is Type)
+ {
+ _output.WriteLine("Server exception: " + ex.ToString());
+ }
+ }, httpOptions: new HttpAgnosticOptions()
+ {
+ UseSsl = useSsl,
+ ClearTextVersion = serverVersion,
+ SslApplicationProtocols = serverVersion.Major >= 2 ? new List<SslApplicationProtocol>{ SslApplicationProtocol.Http2, SslApplicationProtocol.Http11 } : null
+ });
+ }
+
+ [OuterLoop("Uses external server")]
+ [Theory]
+ [MemberData(nameof(VersionSelectionMemberData))]
+ public async Task SendAsync_CorrectVersionSelected_ExternalServer(Version requestVersion, HttpVersionPolicy versionPolicy, Version serverVersion, bool useSsl, object expectedResult)
+ {
+ RemoteServer remoteServer = null;
+ if (serverVersion == HttpVersion.Version11)
+ {
+ remoteServer = useSsl ? RemoteSecureHttp11Server : RemoteHttp11Server;
+ }
+ if (serverVersion == HttpVersion.Version20)
+ {
+ remoteServer = useSsl ? RemoteHttp2Server : null;
+ }
+ // No remote server that could serve the requested version.
+ if (remoteServer == null)
+ {
+ _output.WriteLine($"Skipping test: No remote server that could serve the requested version.");
+ return;
+ }
+
+
+ var request = new HttpRequestMessage(HttpMethod.Get, remoteServer.EchoUri)
+ {
+ Version = requestVersion,
+ VersionPolicy = versionPolicy
+ };
+
+ using HttpClient client = CreateHttpClient();
+ if (expectedResult is Type type)
+ {
+ Exception exception = await Assert.ThrowsAnyAsync<Exception>(() => client.SendAsync(request));
+ Assert.IsType(type, exception);
+ _output.WriteLine(exception.ToString());
+ }
+ else
+ {
+ HttpResponseMessage response = await client.SendAsync(request);
+ Assert.Equal(expectedResult, response.Version);
+ }
+ }
+
[Fact]
public void DefaultRequestVersion_InitialValueExpected()
{
Link="ProductionCode\System\Net\Http\HttpRuleParser.cs" />
<Compile Include="..\..\src\System\Net\Http\HttpUtilities.cs"
Link="ProductionCode\System\Net\Http\HttpUtilities.cs" />
+ <Compile Include="..\..\src\System\Net\Http\HttpVersionPolicy.cs"
+ Link="ProductionCode\System\Net\Http\HttpVersionPolicy.cs" />
<Compile Include="..\..\src\System\Net\Http\MessageProcessingHandler.cs"
Link="ProductionCode\System\Net\Http\MessageProcessingHandler.cs" />
<Compile Include="..\..\src\System\Net\Http\MultipartContent.cs"