Skip to content

Commit f8c007b

Browse files
[OpenApi] Ignore unknown HTTP methods (#63034)
* [OpenApi] Ignore unknown HTTP methods Exclude unsupported HTTP methods from the OpenAPI document, rather than throwing an exception. Resolves #60914. * [OpenApi] Support HTTP QUERY Support generating OpenAPI documentation for HTTP QUERY endpoints. Contributes to #61089.
1 parent 9720579 commit f8c007b

7 files changed

+124
-21
lines changed

src/OpenApi/sample/Controllers/TestController.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Diagnostics.CodeAnalysis;
66
using Microsoft.AspNetCore.Http.HttpResults;
77
using Microsoft.AspNetCore.Mvc;
8+
using Microsoft.AspNetCore.Mvc.Routing;
89

910
[ApiController]
1011
[Route("[controller]")]
@@ -39,6 +40,24 @@ public Ok<CurrentWeather> GetCurrentWeather()
3940
return TypedResults.Ok(new CurrentWeather(1.0f));
4041
}
4142

43+
[Route("/nohttpmethod")]
44+
public ActionResult<CurrentWeather> NoHttpMethod()
45+
=> Ok(new CurrentWeather(-100));
46+
47+
[HttpQuery]
48+
[Route("/query")]
49+
public ActionResult<CurrentWeather> HttpQueryMethod()
50+
=> Ok(new CurrentWeather(0));
51+
52+
[HttpFoo] // See https://github.com/dotnet/aspnetcore/issues/60914
53+
[Route("/unsupported")]
54+
public ActionResult<CurrentWeather> UnsupportedHttpMethod()
55+
=> Ok(new CurrentWeather(100));
56+
57+
public class HttpQuery() : HttpMethodAttribute(["QUERY"]);
58+
59+
public class HttpFoo() : HttpMethodAttribute(["FOO"]);
60+
4261
public class RouteParamsContainer
4362
{
4463
[FromRoute]

src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ internal static class ApiDescriptionExtensions
1616
/// Maps the HTTP method of the ApiDescription to the HttpMethod.
1717
/// </summary>
1818
/// <param name="apiDescription">The ApiDescription to resolve an HttpMethod from.</param>
19-
/// <returns>The <see cref="HttpMethod"/> associated with the given <paramref name="apiDescription"/>.</returns>
20-
public static HttpMethod GetHttpMethod(this ApiDescription apiDescription) =>
19+
/// <returns>The <see cref="HttpMethod"/> associated with the given <paramref name="apiDescription"/>, if known.</returns>
20+
public static HttpMethod? GetHttpMethod(this ApiDescription apiDescription) =>
2121
apiDescription.HttpMethod?.ToUpperInvariant() switch
2222
{
2323
"GET" => HttpMethod.Get,
@@ -28,7 +28,8 @@ public static HttpMethod GetHttpMethod(this ApiDescription apiDescription) =>
2828
"HEAD" => HttpMethod.Head,
2929
"OPTIONS" => HttpMethod.Options,
3030
"TRACE" => HttpMethod.Trace,
31-
_ => throw new InvalidOperationException($"Unsupported HTTP method: {apiDescription.HttpMethod}"),
31+
"QUERY" => HttpMethod.Query,
32+
_ => null,
3233
};
3334

3435
/// <summary>

src/OpenApi/src/Services/OpenApiDocumentService.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,13 @@ private async Task<OpenApiPaths> GetOpenApiPathsAsync(
250250
foreach (var descriptions in descriptionsByPath)
251251
{
252252
Debug.Assert(descriptions.Key != null, "Relative path mapped to OpenApiPath key cannot be null.");
253-
paths.Add(descriptions.Key, new OpenApiPathItem { Operations = await GetOperationsAsync(descriptions, document, scopedServiceProvider, operationTransformers, schemaTransformers, cancellationToken) });
253+
var operations = await GetOperationsAsync(descriptions, document, scopedServiceProvider, operationTransformers, schemaTransformers, cancellationToken);
254+
if (operations.Count > 0)
255+
{
256+
paths.Add(descriptions.Key, new OpenApiPathItem { Operations = operations });
257+
}
254258
}
259+
255260
return paths;
256261
}
257262

@@ -280,7 +285,14 @@ private async Task<Dictionary<HttpMethod, OpenApiOperation>> GetOperationsAsync(
280285
};
281286

282287
_operationTransformerContextCache.TryAdd(description.ActionDescriptor.Id, operationContext);
283-
operations[description.GetHttpMethod()] = operation;
288+
289+
if (description.GetHttpMethod() is not { } method)
290+
{
291+
// Skip unsupported HTTP methods
292+
continue;
293+
}
294+
295+
operations[method] = operation;
284296

285297
// Use index-based for loop to avoid allocating an enumerator with a foreach.
286298
for (var i = 0; i < operationTransformers.Length; i++)

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -63,20 +63,4 @@ public void GetHttpMethod_ReturnsHttpMethodForApiDescription(string httpMethod,
6363
// Assert
6464
Assert.Equal(expectedHttpMethod, result);
6565
}
66-
67-
[Theory]
68-
[InlineData("UNKNOWN")]
69-
[InlineData("unknown")]
70-
public void GetHttpMethod_ThrowsForUnknownHttpMethod(string methodName)
71-
{
72-
// Arrange
73-
var apiDescription = new ApiDescription
74-
{
75-
HttpMethod = methodName
76-
};
77-
78-
// Act & Assert
79-
var exception = Assert.Throws<InvalidOperationException>(() => apiDescription.GetHttpMethod());
80-
Assert.Equal($"Unsupported HTTP method: {methodName}", exception.Message);
81-
}
8266
}

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,35 @@
124124
}
125125
}
126126
}
127+
},
128+
"/query": {
129+
"query": {
130+
"tags": [
131+
"Test"
132+
],
133+
"responses": {
134+
"200": {
135+
"description": "OK",
136+
"content": {
137+
"text/plain": {
138+
"schema": {
139+
"$ref": "#/components/schemas/CurrentWeather"
140+
}
141+
},
142+
"application/json": {
143+
"schema": {
144+
"$ref": "#/components/schemas/CurrentWeather"
145+
}
146+
},
147+
"text/json": {
148+
"schema": {
149+
"$ref": "#/components/schemas/CurrentWeather"
150+
}
151+
}
152+
}
153+
}
154+
}
155+
}
127156
}
128157
},
129158
"components": {

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,35 @@
124124
}
125125
}
126126
}
127+
},
128+
"/query": {
129+
"query": {
130+
"tags": [
131+
"Test"
132+
],
133+
"responses": {
134+
"200": {
135+
"description": "OK",
136+
"content": {
137+
"text/plain": {
138+
"schema": {
139+
"$ref": "#/components/schemas/CurrentWeather"
140+
}
141+
},
142+
"application/json": {
143+
"schema": {
144+
"$ref": "#/components/schemas/CurrentWeather"
145+
}
146+
},
147+
"text/json": {
148+
"schema": {
149+
"$ref": "#/components/schemas/CurrentWeather"
150+
}
151+
}
152+
}
153+
}
154+
}
155+
}
127156
}
128157
},
129158
"components": {

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentLocalizationTests.VerifyOpenApiDocumentIsInvariant.verified.txt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,6 +1233,35 @@
12331233
}
12341234
}
12351235
}
1236+
},
1237+
"/query": {
1238+
"query": {
1239+
"tags": [
1240+
"Test"
1241+
],
1242+
"responses": {
1243+
"200": {
1244+
"description": "OK",
1245+
"content": {
1246+
"text/plain": {
1247+
"schema": {
1248+
"$ref": "#/components/schemas/CurrentWeather"
1249+
}
1250+
},
1251+
"application/json": {
1252+
"schema": {
1253+
"$ref": "#/components/schemas/CurrentWeather"
1254+
}
1255+
},
1256+
"text/json": {
1257+
"schema": {
1258+
"$ref": "#/components/schemas/CurrentWeather"
1259+
}
1260+
}
1261+
}
1262+
}
1263+
}
1264+
}
12361265
}
12371266
},
12381267
"components": {

0 commit comments

Comments
 (0)