Add HttpMethod.Parse (#89270)
authorStephen Toub <stoub@microsoft.com>
Thu, 20 Jul 2023 21:47:50 +0000 (17:47 -0400)
committerGitHub <noreply@github.com>
Thu, 20 Jul 2023 21:47:50 +0000 (17:47 -0400)
src/libraries/System.Net.Http/ref/System.Net.Http.cs
src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.cs
src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Connect.cs
src/libraries/System.Net.Http/tests/FunctionalTests/HttpMethodTest.cs
src/libraries/System.Net.Http/tests/FunctionalTests/MetricsTest.cs
src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs

index a31f5e6..f4794b5 100644 (file)
@@ -244,6 +244,7 @@ namespace System.Net.Http
         public override int GetHashCode() { throw null; }
         public static bool operator ==(System.Net.Http.HttpMethod? left, System.Net.Http.HttpMethod? right) { throw null; }
         public static bool operator !=(System.Net.Http.HttpMethod? left, System.Net.Http.HttpMethod? right) { throw null; }
+        public static System.Net.Http.HttpMethod Parse(ReadOnlySpan<char> method) { throw null; }
         public override string ToString() { throw null; }
     }
     public sealed class HttpProtocolException : System.Net.Http.HttpIOException
index 045d85f..1634453 100644 (file)
@@ -146,6 +146,19 @@ namespace System.Net.Http
             return !(left == right);
         }
 
+        /// <summary>Parses the provided <paramref name="method"/> into an <see cref="HttpMethod"/> instance.</summary>
+        /// <param name="method">The method to parse.</param>
+        /// <returns>An <see cref="HttpMethod"/> instance for the provided <paramref name="method"/>.</returns>
+        /// <remarks>
+        /// This method may return a singleton instance for known methods; for example, it may return <see cref="Get"/>
+        /// if "GET" is specified. The parsing is performed in a case-insensitive manner, so it may also return <see cref="Get"/>
+        /// if "get" is specified. For unknown methods, a new <see cref="HttpMethod"/> instance is returned, with the
+        /// same validation being performed as by the <see cref="HttpMethod(string)"/> constructor.
+        /// </remarks>
+        public static HttpMethod Parse(ReadOnlySpan<char> method) =>
+            GetKnownMethod(method) ??
+            new HttpMethod(method.ToString());
+
         /// <summary>
         /// Returns a singleton method instance with a capitalized method name for the supplied method
         /// if it's known; otherwise, returns the original.
@@ -158,17 +171,23 @@ namespace System.Net.Http
             // _http3Index is only set for the singleton instances, so if it's not null,
             // we can avoid the lookup.  Otherwise, look up the method instance and return the
             // normalized instance if it's found.
+            return method._http3Index is null && GetKnownMethod(method._method) is HttpMethod match ?
+                match :
+                method;
+        }
 
-            if (method._http3Index is null && method._method.Length >= 3) // 3 == smallest known method
+        private static HttpMethod? GetKnownMethod(ReadOnlySpan<char> method)
+        {
+            if (method.Length >= 3) // 3 == smallest known method
             {
-                HttpMethod? match = (method._method[0] | 0x20) switch
+                HttpMethod? match = (method[0] | 0x20) switch
                 {
                     'c' => s_connectMethod,
                     'd' => s_deleteMethod,
                     'g' => s_getMethod,
                     'h' => s_headMethod,
                     'o' => s_optionsMethod,
-                    'p' => method._method.Length switch
+                    'p' => method.Length switch
                     {
                         3 => s_putMethod,
                         4 => s_postMethod,
@@ -178,13 +197,14 @@ namespace System.Net.Http
                     _ => null,
                 };
 
-                if (match is not null && string.Equals(method._method, match._method, StringComparison.OrdinalIgnoreCase))
+                if (match is not null &&
+                    method.Equals(match._method, StringComparison.OrdinalIgnoreCase))
                 {
                     return match;
                 }
             }
 
-            return method;
+            return null;
         }
 
         internal bool MustHaveRequestBody
index 16587b3..3f1f4b3 100644 (file)
@@ -21,7 +21,7 @@ namespace System.Net.Http.Functional.Tests
             {
                 using (HttpClient client = CreateHttpClient())
                 {
-                    HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("CONNECT"), url) { Version = UseVersion };
+                    HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Connect, url) { Version = UseVersion };
                     request.Headers.Host = "foo.com:345";
 
                     // We need to use ResponseHeadersRead here, otherwise we will hang trying to buffer the response body.
@@ -80,7 +80,7 @@ namespace System.Net.Http.Functional.Tests
             {
                 using (HttpClient client = CreateHttpClient())
                 {
-                    HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("CONNECT"), url) { Version = UseVersion };
+                    HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Connect, url) { Version = UseVersion };
                     request.Headers.Host = "foo.com:345";
                     // We need to use ResponseHeadersRead here, otherwise we will hang trying to buffer the response body.
                     Task<HttpResponseMessage> responseTask = client.SendAsync(TestAsync, request,  HttpCompletionOption.ResponseHeadersRead);
index 2a804de..9911fa1 100644 (file)
@@ -150,6 +150,56 @@ namespace System.Net.Http.Functional.Tests
             Assert.Equal("PATCH", HttpMethod.Patch.Method);
         }
 
+        public static IEnumerable<object[]> Parse_UsesKnownInstances_MemberData()
+        {
+            yield return new object[] { HttpMethod.Connect, nameof(HttpMethod.Connect) };
+            yield return new object[] { HttpMethod.Delete, nameof(HttpMethod.Delete) };
+            yield return new object[] { HttpMethod.Get, nameof(HttpMethod.Get) };
+            yield return new object[] { HttpMethod.Head, nameof(HttpMethod.Head) };
+            yield return new object[] { HttpMethod.Options, nameof(HttpMethod.Options) };
+            yield return new object[] { HttpMethod.Patch, nameof(HttpMethod.Patch) };
+            yield return new object[] { HttpMethod.Post, nameof(HttpMethod.Post) };
+            yield return new object[] { HttpMethod.Put, nameof(HttpMethod.Put) };
+            yield return new object[] { HttpMethod.Trace, nameof(HttpMethod.Trace) };
+        }
+
+        [Theory]
+        [MemberData(nameof(Parse_UsesKnownInstances_MemberData))]
+        public void Parse_KnownMethod_UsesKnownInstances(HttpMethod method, string methodName)
+        {
+            Assert.Same(method, HttpMethod.Parse(methodName));
+            Assert.Same(method, HttpMethod.Parse(methodName.ToUpperInvariant()));
+            Assert.Same(method, HttpMethod.Parse(methodName.ToLowerInvariant()));
+        }
+
+        [Theory]
+        [InlineData("Unknown")]
+        [InlineData("custom")]
+        public void Parse_UnknownMethod_UsesNewInstances(string method)
+        {
+            var h = HttpMethod.Parse(method);
+            Assert.NotNull(h);
+            Assert.NotSame(h, HttpMethod.Parse(method));
+        }
+
+        [Theory]
+        [InlineData("")]
+        [InlineData("    ")]
+        public void Parse_Whitespace_ThrowsArgumentException(string method)
+        {
+            AssertExtensions.Throws<ArgumentException>("method", () => HttpMethod.Parse(method));
+        }
+
+        [Theory]
+        [InlineData("  GET  ")]
+        [InlineData(" Post")]
+        [InlineData("Put ")]
+        [InlineData("multiple things")]
+        public void Parse_InvalidToken_Throws(string method)
+        {
+            Assert.Throws<FormatException>(() => HttpMethod.Parse(method));
+        }
+
         private static void AddStaticHttpMethods(List<object[]> staticHttpMethods)
         {
             staticHttpMethods.Add(new object[] { HttpMethod.Patch });
index d57eec5..bb9441a 100644 (file)
@@ -263,7 +263,7 @@ namespace System.Net.Http.Functional.Tests
             {
                 using HttpMessageInvoker client = CreateHttpMessageInvoker();
                 using InstrumentRecorder<double> recorder = SetupInstrumentRecorder<double>(InstrumentNames.RequestDuration);
-                using HttpRequestMessage request = new(new HttpMethod(method), uri) { Version = UseVersion };
+                using HttpRequestMessage request = new(HttpMethod.Parse(method), uri) { Version = UseVersion };
 
                 using HttpResponseMessage response = await SendAsync(client, request);
 
index f99a052..5f39f33 100644 (file)
@@ -1127,7 +1127,7 @@ namespace System.Net
                 throw new InvalidOperationException(SR.net_reqsubmitted);
             }
 
-            var request = new HttpRequestMessage(new HttpMethod(_originVerb), _requestUri);
+            var request = new HttpRequestMessage(HttpMethod.Parse(_originVerb), _requestUri);
 
             bool disposeRequired = false;
             HttpClient? client = null;