Skip to content

Commit a4a8e8e

Browse files
authored
Add HttpMethod.Parse (#89270)
1 parent 8c730dd commit a4a8e8e

File tree

6 files changed

+80
-9
lines changed

6 files changed

+80
-9
lines changed

src/libraries/System.Net.Http/ref/System.Net.Http.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ public HttpMethod(string method) { }
244244
public override int GetHashCode() { throw null; }
245245
public static bool operator ==(System.Net.Http.HttpMethod? left, System.Net.Http.HttpMethod? right) { throw null; }
246246
public static bool operator !=(System.Net.Http.HttpMethod? left, System.Net.Http.HttpMethod? right) { throw null; }
247+
public static System.Net.Http.HttpMethod Parse(ReadOnlySpan<char> method) { throw null; }
247248
public override string ToString() { throw null; }
248249
}
249250
public sealed class HttpProtocolException : System.Net.Http.HttpIOException

src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.cs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,19 @@ public override string ToString()
146146
return !(left == right);
147147
}
148148

149+
/// <summary>Parses the provided <paramref name="method"/> into an <see cref="HttpMethod"/> instance.</summary>
150+
/// <param name="method">The method to parse.</param>
151+
/// <returns>An <see cref="HttpMethod"/> instance for the provided <paramref name="method"/>.</returns>
152+
/// <remarks>
153+
/// This method may return a singleton instance for known methods; for example, it may return <see cref="Get"/>
154+
/// if "GET" is specified. The parsing is performed in a case-insensitive manner, so it may also return <see cref="Get"/>
155+
/// if "get" is specified. For unknown methods, a new <see cref="HttpMethod"/> instance is returned, with the
156+
/// same validation being performed as by the <see cref="HttpMethod(string)"/> constructor.
157+
/// </remarks>
158+
public static HttpMethod Parse(ReadOnlySpan<char> method) =>
159+
GetKnownMethod(method) ??
160+
new HttpMethod(method.ToString());
161+
149162
/// <summary>
150163
/// Returns a singleton method instance with a capitalized method name for the supplied method
151164
/// if it's known; otherwise, returns the original.
@@ -158,17 +171,23 @@ internal static HttpMethod Normalize(HttpMethod method)
158171
// _http3Index is only set for the singleton instances, so if it's not null,
159172
// we can avoid the lookup. Otherwise, look up the method instance and return the
160173
// normalized instance if it's found.
174+
return method._http3Index is null && GetKnownMethod(method._method) is HttpMethod match ?
175+
match :
176+
method;
177+
}
161178

162-
if (method._http3Index is null && method._method.Length >= 3) // 3 == smallest known method
179+
private static HttpMethod? GetKnownMethod(ReadOnlySpan<char> method)
180+
{
181+
if (method.Length >= 3) // 3 == smallest known method
163182
{
164-
HttpMethod? match = (method._method[0] | 0x20) switch
183+
HttpMethod? match = (method[0] | 0x20) switch
165184
{
166185
'c' => s_connectMethod,
167186
'd' => s_deleteMethod,
168187
'g' => s_getMethod,
169188
'h' => s_headMethod,
170189
'o' => s_optionsMethod,
171-
'p' => method._method.Length switch
190+
'p' => method.Length switch
172191
{
173192
3 => s_putMethod,
174193
4 => s_postMethod,
@@ -178,13 +197,14 @@ internal static HttpMethod Normalize(HttpMethod method)
178197
_ => null,
179198
};
180199

181-
if (match is not null && string.Equals(method._method, match._method, StringComparison.OrdinalIgnoreCase))
200+
if (match is not null &&
201+
method.Equals(match._method, StringComparison.OrdinalIgnoreCase))
182202
{
183203
return match;
184204
}
185205
}
186206

187-
return method;
207+
return null;
188208
}
189209

190210
internal bool MustHaveRequestBody

src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Connect.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ await LoopbackServer.CreateServerAsync(async (server, url) =>
2121
{
2222
using (HttpClient client = CreateHttpClient())
2323
{
24-
HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("CONNECT"), url) { Version = UseVersion };
24+
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Connect, url) { Version = UseVersion };
2525
request.Headers.Host = "foo.com:345";
2626

2727
// We need to use ResponseHeadersRead here, otherwise we will hang trying to buffer the response body.
@@ -80,7 +80,7 @@ await LoopbackServer.CreateServerAsync(async (server, url) =>
8080
{
8181
using (HttpClient client = CreateHttpClient())
8282
{
83-
HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("CONNECT"), url) { Version = UseVersion };
83+
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Connect, url) { Version = UseVersion };
8484
request.Headers.Host = "foo.com:345";
8585
// We need to use ResponseHeadersRead here, otherwise we will hang trying to buffer the response body.
8686
Task<HttpResponseMessage> responseTask = client.SendAsync(TestAsync, request, HttpCompletionOption.ResponseHeadersRead);

src/libraries/System.Net.Http/tests/FunctionalTests/HttpMethodTest.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,56 @@ public void Patch_VerifyValue_PropertyNameMatchesHttpMethodName()
150150
Assert.Equal("PATCH", HttpMethod.Patch.Method);
151151
}
152152

153+
public static IEnumerable<object[]> Parse_UsesKnownInstances_MemberData()
154+
{
155+
yield return new object[] { HttpMethod.Connect, nameof(HttpMethod.Connect) };
156+
yield return new object[] { HttpMethod.Delete, nameof(HttpMethod.Delete) };
157+
yield return new object[] { HttpMethod.Get, nameof(HttpMethod.Get) };
158+
yield return new object[] { HttpMethod.Head, nameof(HttpMethod.Head) };
159+
yield return new object[] { HttpMethod.Options, nameof(HttpMethod.Options) };
160+
yield return new object[] { HttpMethod.Patch, nameof(HttpMethod.Patch) };
161+
yield return new object[] { HttpMethod.Post, nameof(HttpMethod.Post) };
162+
yield return new object[] { HttpMethod.Put, nameof(HttpMethod.Put) };
163+
yield return new object[] { HttpMethod.Trace, nameof(HttpMethod.Trace) };
164+
}
165+
166+
[Theory]
167+
[MemberData(nameof(Parse_UsesKnownInstances_MemberData))]
168+
public void Parse_KnownMethod_UsesKnownInstances(HttpMethod method, string methodName)
169+
{
170+
Assert.Same(method, HttpMethod.Parse(methodName));
171+
Assert.Same(method, HttpMethod.Parse(methodName.ToUpperInvariant()));
172+
Assert.Same(method, HttpMethod.Parse(methodName.ToLowerInvariant()));
173+
}
174+
175+
[Theory]
176+
[InlineData("Unknown")]
177+
[InlineData("custom")]
178+
public void Parse_UnknownMethod_UsesNewInstances(string method)
179+
{
180+
var h = HttpMethod.Parse(method);
181+
Assert.NotNull(h);
182+
Assert.NotSame(h, HttpMethod.Parse(method));
183+
}
184+
185+
[Theory]
186+
[InlineData("")]
187+
[InlineData(" ")]
188+
public void Parse_Whitespace_ThrowsArgumentException(string method)
189+
{
190+
AssertExtensions.Throws<ArgumentException>("method", () => HttpMethod.Parse(method));
191+
}
192+
193+
[Theory]
194+
[InlineData(" GET ")]
195+
[InlineData(" Post")]
196+
[InlineData("Put ")]
197+
[InlineData("multiple things")]
198+
public void Parse_InvalidToken_Throws(string method)
199+
{
200+
Assert.Throws<FormatException>(() => HttpMethod.Parse(method));
201+
}
202+
153203
private static void AddStaticHttpMethods(List<object[]> staticHttpMethods)
154204
{
155205
staticHttpMethods.Add(new object[] { HttpMethod.Patch });

src/libraries/System.Net.Http/tests/FunctionalTests/MetricsTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ public Task RequestDuration_Success_Recorded(string method, HttpStatusCode statu
263263
{
264264
using HttpMessageInvoker client = CreateHttpMessageInvoker();
265265
using InstrumentRecorder<double> recorder = SetupInstrumentRecorder<double>(InstrumentNames.RequestDuration);
266-
using HttpRequestMessage request = new(new HttpMethod(method), uri) { Version = UseVersion };
266+
using HttpRequestMessage request = new(HttpMethod.Parse(method), uri) { Version = UseVersion };
267267

268268
using HttpResponseMessage response = await SendAsync(client, request);
269269

src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1127,7 +1127,7 @@ private async Task<WebResponse> SendRequest(bool async)
11271127
throw new InvalidOperationException(SR.net_reqsubmitted);
11281128
}
11291129

1130-
var request = new HttpRequestMessage(new HttpMethod(_originVerb), _requestUri);
1130+
var request = new HttpRequestMessage(HttpMethod.Parse(_originVerb), _requestUri);
11311131

11321132
bool disposeRequired = false;
11331133
HttpClient? client = null;

0 commit comments

Comments
 (0)