Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/libraries/System.Net.Http/ref/System.Net.Http.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ public HttpMethod(string method) { }
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
Expand Down
30 changes: 25 additions & 5 deletions src/libraries/System.Net.Http/src/System/Net/Http/HttpMethod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,19 @@ public override string ToString()
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.
Expand All @@ -158,17 +171,23 @@ internal static HttpMethod Normalize(HttpMethod method)
// _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,
Expand All @@ -178,13 +197,14 @@ internal static HttpMethod Normalize(HttpMethod method)
_ => 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ await LoopbackServer.CreateServerAsync(async (server, url) =>
{
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.
Expand Down Expand Up @@ -80,7 +80,7 @@ await LoopbackServer.CreateServerAsync(async (server, url) =>
{
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,56 @@ public void Patch_VerifyValue_PropertyNameMatchesHttpMethodName()
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 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ public Task RequestDuration_Success_Recorded(string method, HttpStatusCode statu
{
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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1127,7 +1127,7 @@ private async Task<WebResponse> SendRequest(bool async)
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;
Expand Down