diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs index 98c23bdcde0c0..54497e28618f7 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs @@ -1950,5 +1950,23 @@ public async Task GetAsync_InvalidUrl_ExpectedExceptionThrown() await Assert.ThrowsAsync(() => client.GetStringAsync(invalidUri)); } } + + // HttpRequestMessage ctor guards against such Uris before .NET 6. We allow passing relative/unknown Uris to BrowserHttpHandler. + public static bool InvalidRequestUriTest_IsSupported => PlatformDetection.IsNotNetFramework && PlatformDetection.IsNotBrowser; + + [ConditionalFact(nameof(InvalidRequestUriTest_IsSupported))] + public async Task SendAsync_InvalidRequestUri_Throws() + { + using var invoker = new HttpMessageInvoker(CreateHttpClientHandler()); + + var request = new HttpRequestMessage(HttpMethod.Get, (Uri)null); + await Assert.ThrowsAsync(() => invoker.SendAsync(request, CancellationToken.None)); + + request = new HttpRequestMessage(HttpMethod.Get, new Uri("/relative", UriKind.Relative)); + await Assert.ThrowsAsync(() => invoker.SendAsync(request, CancellationToken.None)); + + request = new HttpRequestMessage(HttpMethod.Get, new Uri("foo://foo.bar")); + await Assert.ThrowsAsync(() => invoker.SendAsync(request, CancellationToken.None)); + } } } diff --git a/src/libraries/System.Net.Http.WinHttpHandler/src/Resources/Strings.resx b/src/libraries/System.Net.Http.WinHttpHandler/src/Resources/Strings.resx index 3f4beab8dd846..4fcb089cc506d 100644 --- a/src/libraries/System.Net.Http.WinHttpHandler/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Http.WinHttpHandler/src/Resources/Strings.resx @@ -126,4 +126,10 @@ WinHttpHandler is only supported on .NET Framework and .NET runtimes on Windows. It is not supported for Windows Store Applications (UWP) or Unix platforms. + + The '{0}' scheme is not supported. + + + An invalid request URI was provided. Either the request URI must be an absolute URI or BaseAddress must be set. + diff --git a/src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpHandler.cs b/src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpHandler.cs index 63bd0aaf92f36..9ce26b2716d24 100644 --- a/src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpHandler.cs +++ b/src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpHandler.cs @@ -578,6 +578,17 @@ protected override void Dispose(bool disposing) throw new ArgumentNullException(nameof(request), SR.net_http_handler_norequest); } + 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) { diff --git a/src/libraries/System.Net.Http/src/Resources/Strings.resx b/src/libraries/System.Net.Http/src/Resources/Strings.resx index 35ce6f88feb33..03081881762ef 100644 --- a/src/libraries/System.Net.Http/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Http/src/Resources/Strings.resx @@ -196,13 +196,10 @@ The base address must be an absolute URI. - An invalid request URI was provided. The request URI must either be an absolute URI or BaseAddress must be set. + An invalid request URI was provided. Either the request URI must be an absolute URI or BaseAddress must be set. - - Only 'http' and 'https' schemes are allowed. - - - Only 'http', 'https', and 'blob' schemes are allowed. + + The '{0}' scheme is not supported. Value '{0}' is not a valid Base64 string. Error: {1} diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index bea0e12f9fe00..49bc41d756e4b 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -53,7 +53,6 @@ - @@ -180,6 +179,7 @@ + @@ -188,7 +188,6 @@ - @@ -623,7 +622,6 @@ - diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/HttpUtilities.Browser.cs b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/HttpUtilities.Browser.cs deleted file mode 100644 index 6e9b4b027fefe..0000000000000 --- a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/HttpUtilities.Browser.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.Threading.Tasks; -using System.Threading; - -namespace System.Net.Http -{ - internal static partial class HttpUtilities - { - internal static bool IsSupportedNonSecureScheme(string scheme) => - string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase) - || IsBlobScheme(scheme) - || IsNonSecureWebSocketScheme(scheme); - - internal static bool IsBlobScheme(string scheme) => - string.Equals(scheme, "blob", StringComparison.OrdinalIgnoreCase); - - internal static string InvalidUriMessage => SR.net_http_client_http_browser_baseaddress_required; - } -} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs index 514744fee99d5..27b0c357cc686 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs @@ -26,8 +26,8 @@ public partial class HttpClient : HttpMessageInvoker private CancellationTokenSource _pendingRequestsCts; private HttpRequestHeaders? _defaultRequestHeaders; - private Version _defaultRequestVersion = HttpUtilities.DefaultRequestVersion; - private HttpVersionPolicy _defaultVersionPolicy = HttpUtilities.DefaultVersionPolicy; + private Version _defaultRequestVersion = HttpRequestMessage.DefaultRequestVersion; + private HttpVersionPolicy _defaultVersionPolicy = HttpRequestMessage.DefaultVersionPolicy; private Uri? _baseAddress; private TimeSpan _timeout; @@ -78,7 +78,12 @@ public HttpVersionPolicy DefaultVersionPolicy get => _baseAddress; set { - CheckBaseAddress(value, nameof(value)); + // It's OK to not have a base address specified, but if one is, it needs to be absolute. + if (value is not null && !value.IsAbsoluteUri) + { + throw new ArgumentException(SR.net_http_client_absolute_baseaddress_required, nameof(value)); + } + CheckDisposedOrStarted(); if (NetEventSource.Log.IsEnabled()) NetEventSource.UriBaseAddress(this, value); @@ -621,7 +626,7 @@ private void HandleFailure(Exception e, bool telemetryStarted, HttpResponseMessa private static bool StartSend(HttpRequestMessage request) { - if (HttpTelemetry.Log.IsEnabled() && request.RequestUri != null) + if (HttpTelemetry.Log.IsEnabled()) { HttpTelemetry.Log.RequestStart(request); return true; @@ -810,24 +815,6 @@ private void PrepareRequestMessage(HttpRequestMessage request) return (pendingRequestsCts, DisposeTokenSource: false, pendingRequestsCts); } - private static void CheckBaseAddress(Uri? baseAddress, string parameterName) - { - if (baseAddress == null) - { - return; // It's OK to not have a base address specified. - } - - if (!baseAddress.IsAbsoluteUri) - { - throw new ArgumentException(SR.net_http_client_absolute_baseaddress_required, parameterName); - } - - if (!HttpUtilities.IsHttpUri(baseAddress)) - { - throw new ArgumentException(HttpUtilities.InvalidUriMessage, parameterName); - } - } - private static bool IsNativeHandlerEnabled() { if (!AppContext.TryGetSwitch("System.Net.Http.UseNativeHttpHandler", out bool isEnabled)) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpMessageInvoker.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpMessageInvoker.cs index 4988469f1ec03..9b33c75b4b22d 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpMessageInvoker.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpMessageInvoker.cs @@ -34,8 +34,7 @@ public HttpMessageInvoker(HttpMessageHandler handler, bool disposeHandler) } [UnsupportedOSPlatformAttribute("browser")] - public virtual HttpResponseMessage Send(HttpRequestMessage request, - CancellationToken cancellationToken) + public virtual HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) { if (request == null) { @@ -43,7 +42,7 @@ public HttpMessageInvoker(HttpMessageHandler handler, bool disposeHandler) } CheckDisposed(); - if (HttpTelemetry.Log.IsEnabled() && !request.WasSentByHttpClient() && request.RequestUri != null) + if (ShouldSendWithTelemetry(request)) { HttpTelemetry.Log.RequestStart(request); @@ -67,8 +66,7 @@ public HttpMessageInvoker(HttpMessageHandler handler, bool disposeHandler) } } - public virtual Task SendAsync(HttpRequestMessage request, - CancellationToken cancellationToken) + public virtual Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (request == null) { @@ -76,7 +74,7 @@ public HttpMessageInvoker(HttpMessageHandler handler, bool disposeHandler) } CheckDisposed(); - if (HttpTelemetry.Log.IsEnabled() && !request.WasSentByHttpClient() && request.RequestUri != null) + if (ShouldSendWithTelemetry(request)) { return SendAsyncWithTelemetry(_handler, request, cancellationToken); } @@ -103,6 +101,12 @@ static async Task SendAsyncWithTelemetry(HttpMessageHandler } } + private static bool ShouldSendWithTelemetry(HttpRequestMessage request) => + HttpTelemetry.Log.IsEnabled() && + !request.WasSentByHttpClient() && + request.RequestUri is Uri requestUri && + requestUri.IsAbsoluteUri; + internal static bool LogRequestFailed(bool telemetryStarted) { if (HttpTelemetry.Log.IsEnabled() && telemetryStarted) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestMessage.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestMessage.cs index eaa59cd022a88..d0c88d60f7714 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestMessage.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestMessage.cs @@ -1,17 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Collections.Generic; -using System.Diagnostics; namespace System.Net.Http { public class HttpRequestMessage : IDisposable { + internal static Version DefaultRequestVersion => HttpVersion.Version11; + internal static HttpVersionPolicy DefaultVersionPolicy => HttpVersionPolicy.RequestVersionOrLower; + private const int MessageNotYetSent = 0; private const int MessageAlreadySent = 1; @@ -101,29 +102,12 @@ public HttpMethod Method get { return _requestUri; } set { - if ((value != null) && (value.IsAbsoluteUri) && (!HttpUtilities.IsHttpUri(value))) - { - throw new ArgumentException(HttpUtilities.InvalidUriMessage, nameof(value)); - } CheckDisposed(); - - // It's OK to set 'null'. HttpClient will add the 'BaseAddress'. If there is no 'BaseAddress' - // sending this message will throw. _requestUri = value; } } - public HttpRequestHeaders Headers - { - get - { - if (_headers == null) - { - _headers = new HttpRequestHeaders(); - } - return _headers; - } - } + public HttpRequestHeaders Headers => _headers ??= new HttpRequestHeaders(); internal bool HasHeaders => _headers != null; @@ -139,22 +123,18 @@ public HttpRequestMessage() public HttpRequestMessage(HttpMethod method, Uri? requestUri) { - InitializeValues(method, requestUri); + // It's OK to have a 'null' request Uri. If HttpClient is used, the 'BaseAddress' will be added. + // If there is no 'BaseAddress', sending this request message will throw. + // Note that we also allow the string to be empty: null and empty are considered equivalent. + _method = method ?? throw new ArgumentNullException(nameof(method)); + _requestUri = requestUri; + _version = DefaultRequestVersion; + _versionPolicy = DefaultVersionPolicy; } public HttpRequestMessage(HttpMethod method, string? requestUri) + : this(method, string.IsNullOrEmpty(requestUri) ? null : new Uri(requestUri, UriKind.RelativeOrAbsolute)) { - // It's OK to have a 'null' request Uri. If HttpClient is used, the 'BaseAddress' will be added. - // If there is no 'BaseAddress', sending this request message will throw. - // Note that we also allow the string to be empty: null and empty are considered equivalent. - if (string.IsNullOrEmpty(requestUri)) - { - InitializeValues(method, null); - } - else - { - InitializeValues(method, new Uri(requestUri, UriKind.RelativeOrAbsolute)); - } } public override string ToString() @@ -179,25 +159,6 @@ public override string ToString() return sb.ToString(); } - [MemberNotNull(nameof(_method))] - [MemberNotNull(nameof(_version))] - private void InitializeValues(HttpMethod method, Uri? requestUri) - { - if (method is null) - { - throw new ArgumentNullException(nameof(method)); - } - if ((requestUri != null) && (requestUri.IsAbsoluteUri) && (!HttpUtilities.IsHttpUri(requestUri))) - { - throw new ArgumentException(HttpUtilities.InvalidUriMessage, nameof(requestUri)); - } - - _method = method; - _requestUri = requestUri; - _version = HttpUtilities.DefaultRequestVersion; - _versionPolicy = HttpUtilities.DefaultVersionPolicy; - } - internal bool MarkAsSent() { return Interlocked.Exchange(ref _sendStatus, MessageAlreadySent) == MessageNotYetSent; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs index 6af856e3585f7..49f6a6f34a72e 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs @@ -10,7 +10,8 @@ namespace System.Net.Http { public class HttpResponseMessage : IDisposable { - private const HttpStatusCode defaultStatusCode = HttpStatusCode.OK; + private const HttpStatusCode DefaultStatusCode = HttpStatusCode.OK; + private static Version DefaultResponseVersion => HttpVersion.Version11; private HttpStatusCode _statusCode; private HttpResponseHeaders? _headers; @@ -149,7 +150,7 @@ public bool IsSuccessStatusCode } public HttpResponseMessage() - : this(defaultStatusCode) + : this(DefaultStatusCode) { } @@ -161,7 +162,7 @@ public HttpResponseMessage(HttpStatusCode statusCode) } _statusCode = statusCode; - _version = HttpUtilities.DefaultResponseVersion; + _version = DefaultResponseVersion; } public HttpResponseMessage EnsureSuccessStatusCode() diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpTelemetry.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpTelemetry.cs index a008637271e1a..ae52766e08116 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpTelemetry.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpTelemetry.cs @@ -36,7 +36,7 @@ private void RequestStart(string scheme, string host, int port, string pathAndQu [NonEvent] public void RequestStart(HttpRequestMessage request) { - Debug.Assert(request.RequestUri != null); + Debug.Assert(request.RequestUri != null && request.RequestUri.IsAbsoluteUri); RequestStart( request.RequestUri.Scheme, diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.AnyOS.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.AnyOS.cs deleted file mode 100644 index afa2d7d855133..0000000000000 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.AnyOS.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.Threading.Tasks; -using System.Threading; - -namespace System.Net.Http -{ - internal static partial class HttpUtilities - { - internal static bool IsSupportedNonSecureScheme(string scheme) => - string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase) - || IsNonSecureWebSocketScheme(scheme); - - internal static string InvalidUriMessage => SR.net_http_client_http_baseaddress_required; - } -} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/MessageProcessingHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/MessageProcessingHandler.cs index f903186995e70..d0c3748d4943d 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/MessageProcessingHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/MessageProcessingHandler.cs @@ -63,7 +63,7 @@ protected MessageProcessingHandler(HttpMessageHandler innerHandler) // We schedule a continuation task once the inner handler completes in order to trigger the response // processing method. ProcessResponse() is only called if the task wasn't canceled before. - sendAsyncTask.ContinueWithStandard(tcs, static (task, state) => + sendAsyncTask.ContinueWith(static (task, state) => { var sendState = (SendState)state!; MessageProcessingHandler self = sendState._handler; @@ -106,7 +106,7 @@ protected MessageProcessingHandler(HttpMessageHandler innerHandler) // if the operation was canceled: We'll set the Task returned to the user to canceled. Passing the // cancellation token here would result in the continuation task to not be called at all. I.e. we // would never complete the task returned to the caller of SendAsync(). - }); + }, tcs, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } catch (OperationCanceledException e) { diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpUtilities.cs similarity index 50% rename from src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs rename to src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpUtilities.cs index 8c9d786fd49d2..1c2f442d3e21a 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpUtilities.cs @@ -1,30 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; -using System.Threading.Tasks; -using System.Threading; - namespace System.Net.Http { - internal static partial class HttpUtilities + internal static class HttpUtilities { - internal static Version DefaultRequestVersion => HttpVersion.Version11; - - internal static Version DefaultResponseVersion => HttpVersion.Version11; - - internal static HttpVersionPolicy DefaultVersionPolicy => HttpVersionPolicy.RequestVersionOrLower; - - internal static bool IsHttpUri(Uri uri) - { - Debug.Assert(uri != null); - return IsSupportedScheme(uri.Scheme); - } - internal static bool IsSupportedScheme(string scheme) => IsSupportedNonSecureScheme(scheme) || IsSupportedSecureScheme(scheme); + internal static bool IsSupportedNonSecureScheme(string scheme) => + string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase) || IsNonSecureWebSocketScheme(scheme); + internal static bool IsSupportedSecureScheme(string scheme) => string.Equals(scheme, "https", StringComparison.OrdinalIgnoreCase) || IsSecureWebSocketScheme(scheme); @@ -41,16 +28,5 @@ internal static bool IsHttpUri(Uri uri) string.Equals(scheme, "socks5", StringComparison.OrdinalIgnoreCase) || string.Equals(scheme, "socks4a", StringComparison.OrdinalIgnoreCase) || string.Equals(scheme, "socks4", StringComparison.OrdinalIgnoreCase); - - // Always specify TaskScheduler.Default to prevent us from using a user defined TaskScheduler.Current. - // - // Since we're not doing any CPU and/or I/O intensive operations, continue on the same thread. - // This results in better performance since the continuation task doesn't get scheduled by the - // scheduler and there are no context switches required. - internal static Task ContinueWithStandard(this Task task, object state, Action, object?> continuation) - { - return task.ContinueWith(continuation, state, CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs index 157ef2dde7e4c..42361fba08085 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs @@ -2,10 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Diagnostics; using System.IO; -using System.Net.Quic; -using System.Net.Quic.Implementations; using System.Net.Security; using System.Runtime.Versioning; using System.Threading; @@ -608,6 +605,17 @@ protected internal override Task SendAsync(HttpRequestMessa } } + Uri? requestUri = request.RequestUri; + if (requestUri is null || !requestUri.IsAbsoluteUri) + { + return new InvalidOperationException(SR.net_http_client_invalid_requesturi); + } + + if (!HttpUtilities.IsSupportedScheme(requestUri.Scheme)) + { + return new NotSupportedException(SR.Format(SR.net_http_unsupported_requesturi_scheme, requestUri.Scheme)); + } + return null; } } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs index a5448b012d238..61727faa43307 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs @@ -61,11 +61,21 @@ public void BaseAddress_InvalidUri_Throws() { using (var client = new HttpClient(new CustomResponseHandler((r, c) => Task.FromResult(new HttpResponseMessage())))) { - AssertExtensions.Throws("value", () => client.BaseAddress = new Uri("ftp://onlyhttpsupported")); AssertExtensions.Throws("value", () => client.BaseAddress = new Uri("/onlyabsolutesupported", UriKind.Relative)); } } + [Fact] + public void BaseAddress_UnknownScheme_DoesNotThrow() + { + using (var client = new HttpClient(new CustomResponseHandler((r, c) => Task.FromResult(new HttpResponseMessage())))) + { + client.BaseAddress = new Uri("blob://foo.bar"); + client.BaseAddress = new Uri("extensions://foo.bar"); + client.BaseAddress = new Uri("foobar://foo.bar"); + } + } + [Fact] public void Timeout_Roundtrip_Equal() { diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpRequestMessageTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpRequestMessageTest.cs index 18b7fa9fe47cc..5b936425da168 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpRequestMessageTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpRequestMessageTest.cs @@ -63,6 +63,17 @@ public void Ctor_NullStringUri_Accepted() Assert.Null(rm.Content); } + [Fact] + public void Ctor_EmptyStringUri_Accepted() + { + var rm = new HttpRequestMessage(HttpMethod.Put, string.Empty); + + Assert.Null(rm.RequestUri); + Assert.Equal(HttpMethod.Put, rm.Method); + Assert.Equal(_expectedRequestMessageVersion, rm.Version); + Assert.Null(rm.Content); + } + [Fact] public void Ctor_RelativeUri_CorrectValues() { @@ -105,9 +116,9 @@ public void Ctor_NullMethod_ThrowsArgumentNullException() } [Fact] - public void Ctor_NonHttpUri_ThrowsArgumentException() + public void Ctor_NonHttpUri_DoesNotThrow() { - AssertExtensions.Throws("requestUri", () => new HttpRequestMessage(HttpMethod.Put, "ftp://example.com")); + new HttpRequestMessage(HttpMethod.Put, "ftp://example.com"); } [Fact] @@ -159,10 +170,10 @@ public void Properties_SetPropertiesAndGetTheirValue_MatchingValues() } [Fact] - public void RequestUri_SetNonHttpUri_ThrowsArgumentException() + public void RequestUri_SetNonHttpUri_DoesNotThrow() { var rm = new HttpRequestMessage(); - AssertExtensions.Throws("value", () => { rm.RequestUri = new Uri("ftp://example.com"); }); + rm.RequestUri = new Uri("ftp://example.com"); } [Fact] diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs index 10e81d5f67919..d8b7ec4e712cd 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs @@ -3205,4 +3205,100 @@ public sealed class SocketsHttpHandler_HttpClientHandler_Finalization_Http3_Mock protected override Version UseVersion => HttpVersion.Version30; protected override QuicImplementationProvider UseQuicImplementationProvider => QuicImplementationProviders.Mock; } + + [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + public abstract class SocketsHttpHandler_RequestValidationTest + { + protected abstract bool TestAsync { get; } + + [Fact] + public void Send_NullRequest_ThrowsArgumentNullException() + { + Assert.Throws("request", () => + { + var invoker = new HttpMessageInvoker(new SocketsHttpHandler()); + if (TestAsync) + { + invoker.SendAsync(null, CancellationToken.None); + } + else + { + invoker.Send(null, CancellationToken.None); + } + }); + } + + [Fact] + public void Send_NullRequestUri_ThrowsInvalidOperationException() + { + Throws(new HttpRequestMessage()); + } + + [Fact] + public void Send_RelativeRequestUri_ThrowsInvalidOperationException() + { + Throws(new HttpRequestMessage(HttpMethod.Get, new Uri("/relative", UriKind.Relative))); + } + + [Fact] + public void Send_UnsupportedRequestUriScheme_ThrowsNotSupportedException() + { + Throws(new HttpRequestMessage(HttpMethod.Get, "foo://foo.bar")); + } + + [Fact] + public void Send_MajorVersionZero_ThrowsNotSupportedException() + { + Throws(new HttpRequestMessage { Version = new Version(0, 42) }); + } + + [Fact] + public void Send_TransferEncodingChunkedWithNoContent_ThrowsHttpRequestException() + { + var request = new HttpRequestMessage(); + request.Headers.TransferEncodingChunked = true; + + HttpRequestException exception = Throws(request); + Assert.IsType(exception.InnerException); + } + + [Fact] + public void Send_Http10WithTransferEncodingChunked_ThrowsNotSupportedException() + { + var request = new HttpRequestMessage + { + Content = new StringContent("foo"), + Version = new Version(1, 0) + }; + request.Headers.TransferEncodingChunked = true; + + Throws(request); + } + + private TException Throws(HttpRequestMessage request) + where TException : Exception + { + var invoker = new HttpMessageInvoker(new SocketsHttpHandler()); + if (TestAsync) + { + Task task = invoker.SendAsync(request, CancellationToken.None); + Assert.Equal(TaskStatus.Faulted, task.Status); + return Assert.IsType(task.Exception.InnerException); + } + else + { + return Assert.Throws(() => invoker.Send(request, CancellationToken.None)); + } + } + } + + public sealed class SocketsHttpHandler_RequestValidationTest_Async : SocketsHttpHandler_RequestValidationTest + { + protected override bool TestAsync => true; + } + + public sealed class SocketsHttpHandler_RequestValidationTest_Sync : SocketsHttpHandler_RequestValidationTest + { + protected override bool TestAsync => false; + } } diff --git a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj index 3b950f2a36c9f..d6a2cc28d1113 100644 --- a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj @@ -218,8 +218,6 @@ Link="ProductionCode\System\Net\Http\HttpResponseMessage.cs" /> - + - diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/Http/HttpRequestMessageTest.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/Http/HttpRequestMessageTest.cs index 42e34d74156dd..39bbf05fef4bd 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/Http/HttpRequestMessageTest.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/Http/HttpRequestMessageTest.cs @@ -46,6 +46,7 @@ public void Ctor_RelativeStringUri_CorrectValues() [Theory] [InlineData("http://host/absolute/")] [InlineData("blob:http://host/absolute/")] + [InlineData("foo://host/absolute")] public void Ctor_AbsoluteStringUri_CorrectValues(string uri) { var rm = new HttpRequestMessage(HttpMethod.Post, uri); @@ -82,6 +83,7 @@ public void Ctor_RelativeUri_CorrectValues() [Theory] [InlineData("http://host/absolute/")] [InlineData("blob:http://host/absolute/")] + [InlineData("foo://host/absolute")] public void Ctor_AbsoluteUri_CorrectValues(string uriData) { var uri = new Uri(uriData); @@ -112,12 +114,6 @@ public void Ctor_NullMethod_ThrowsArgumentNullException(string uriData) Assert.Throws(() => new HttpRequestMessage(null, uriData)); } - [Fact] - public void Ctor_NonHttpUri_ThrowsArgumentException() - { - AssertExtensions.Throws("requestUri", () => new HttpRequestMessage(HttpMethod.Put, "ftp://example.com")); - } - [Theory] [InlineData("http://example.com")] [InlineData("blob:http://example.com")] @@ -304,14 +300,6 @@ public void Properties_SetOptionsAndGetTheirValue_NotSet_EnableStreamingResponse Assert.False(streamingEnabledValue); } - - [Fact] - public void RequestUri_SetNonHttpUri_ThrowsArgumentException() - { - var rm = new HttpRequestMessage(); - AssertExtensions.Throws("value", () => { rm.RequestUri = new Uri("ftp://example.com"); }); - } - [Fact] public void Version_SetToNull_ThrowsArgumentNullException() {