Skip to content

Commit ef3b406

Browse files
authored
Respect user set Type for client errors via ProducesResponseType (#30509)
* Respect user set Type for client errors via ProducesResponseType * Address feedback from PR review * React to name feedback * Maintain existing behavior where IsDefaultResponse=true
1 parent 58e9360 commit ef3b406

File tree

4 files changed

+66
-3
lines changed

4 files changed

+66
-3
lines changed

src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,15 @@ private ICollection<ApiResponseType> GetApiResponseTypes(
109109
// from the return type.
110110
apiResponseType.Type = type;
111111
}
112-
else if (IsClientError(statusCode) || apiResponseType.IsDefaultResponse)
112+
else if (IsClientError(statusCode))
113+
{
114+
// Determine whether or not the type was provided by the user. If so, favor it over the default
115+
// error type for 4xx client errors if no response type is specified..
116+
var setByDefault = metadataAttribute is ProducesResponseTypeAttribute { IsResponseTypeSetByDefault: true };
117+
apiResponseType.Type = setByDefault ? defaultErrorType : apiResponseType.Type;
118+
}
119+
else if (apiResponseType.IsDefaultResponse)
113120
{
114-
// Use the default error type for "default" responses or 4xx client errors if no response type is specified.
115121
apiResponseType.Type = defaultErrorType;
116122
}
117123
}

src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ public class ProducesResponseTypeAttribute : Attribute, IApiResponseMetadataProv
1515
{
1616
/// <summary>
1717
/// Initializes an instance of <see cref="ProducesResponseTypeAttribute"/>.
18-
/// </summary>
18+
/// </summary>
1919
/// <param name="statusCode">The HTTP response status code.</param>
2020
public ProducesResponseTypeAttribute(int statusCode)
2121
: this(typeof(void), statusCode)
2222
{
23+
IsResponseTypeSetByDefault = true;
2324
}
2425

2526
/// <summary>
@@ -31,6 +32,7 @@ public ProducesResponseTypeAttribute(Type type, int statusCode)
3132
{
3233
Type = type ?? throw new ArgumentNullException(nameof(type));
3334
StatusCode = statusCode;
35+
IsResponseTypeSetByDefault = false;
3436
}
3537

3638
/// <summary>
@@ -43,6 +45,18 @@ public ProducesResponseTypeAttribute(Type type, int statusCode)
4345
/// </summary>
4446
public int StatusCode { get; set; }
4547

48+
/// <summary>
49+
/// Used to distinguish a `Type` set by default in the constructor versus
50+
/// one provided by the user.
51+
///
52+
/// When <see langword="false"/>, then <see cref="Type"/> is set by user.
53+
///
54+
/// When <see langword="true"/>, then <see cref="Type"/> is set by by
55+
/// default in the constructor
56+
/// </summary>
57+
/// <value></value>
58+
internal bool IsResponseTypeSetByDefault { get; }
59+
4660
/// <inheritdoc />
4761
void IApiResponseMetadataProvider.SetContentTypes(MediaTypeCollection contentTypes)
4862
{

src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,6 +1461,29 @@ public async Task ApiConvention_ForActionWithApiConventionMethod()
14611461
});
14621462
}
14631463

1464+
[Theory]
1465+
[InlineData("ActionWithNoExplicitType", typeof(ProblemDetails))]
1466+
[InlineData("ActionWithVoidType", typeof(void))]
1467+
public async Task ApiAction_ForActionWithVoidResponseType(string path, Type type)
1468+
{
1469+
// Act
1470+
var response = await Client.GetAsync($"http://localhost/ApiExplorerVoid/{path}");
1471+
1472+
var responseBody = await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
1473+
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(responseBody);
1474+
1475+
// Assert
1476+
var description = Assert.Single(result);
1477+
Assert.Collection(
1478+
description.SupportedResponseTypes.OrderBy(r => r.StatusCode),
1479+
responseType =>
1480+
{
1481+
Assert.Equal(type.FullName, responseType.ResponseType);
1482+
Assert.Equal(401, responseType.StatusCode);
1483+
Assert.False(responseType.IsDefaultResponse);
1484+
});
1485+
}
1486+
14641487
private IEnumerable<string> GetSortedMediaTypes(ApiExplorerResponseType apiResponseType)
14651488
{
14661489
return apiResponseType.ResponseFormats
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
2+
// Copyright (c) .NET Foundation. All rights reserved.
3+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
4+
5+
using Microsoft.AspNetCore.Mvc;
6+
7+
namespace ApiExplorerWebSite
8+
{
9+
[Route("ApiExplorerVoid/[action]")]
10+
[ApiController]
11+
public class ApiExplorerVoidController : Controller
12+
{
13+
[ProducesResponseType(typeof(void), 401)]
14+
public IActionResult ActionWithVoidType() => Ok();
15+
16+
[ProducesResponseType(401)]
17+
public IActionResult ActionWithNoExplicitType() => Ok();
18+
19+
}
20+
}

0 commit comments

Comments
 (0)