Skip to content

Commit 6ca55f2

Browse files
Support 406 and 415 with ProblemDetails. Resolves #886
1 parent ad2372a commit 6ca55f2

File tree

17 files changed

+161
-50
lines changed

17 files changed

+161
-50
lines changed

src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingMediaType/Controllers/Values2Controller.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@
22

33
namespace Asp.Versioning.Http.UsingMediaType.Controllers;
44

5+
using Newtonsoft.Json.Linq;
56
using System.Web.Http;
67

78
[ApiVersion( "2.0" )]
8-
[Route( "api/values" )]
9+
[RoutePrefix( "api/values" )]
910
public class Values2Controller : ApiController
1011
{
11-
public IHttpActionResult Get() => Ok( new { controller = GetType().Name, version = Request.GetRequestedApiVersion().ToString() } );
12+
[Route]
13+
public IHttpActionResult Get() =>
14+
Ok( new { controller = GetType().Name, version = Request.GetRequestedApiVersion().ToString() } );
15+
16+
[Route( "{id}", Name = "GetByIdV2" )]
17+
public IHttpActionResult Get( string id ) =>
18+
Ok( new { controller = GetType().Name, Id = id, version = Request.GetRequestedApiVersion().ToString() } );
19+
20+
public IHttpActionResult Post( [FromBody] JToken json ) =>
21+
CreatedAtRoute( "GetByIdV2", new { id = "42" }, json );
1222
}

src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingMediaType/Controllers/ValuesController.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@ namespace Asp.Versioning.Http.UsingMediaType.Controllers;
55
using System.Web.Http;
66

77
[ApiVersion( "1.0" )]
8-
[Route( "api/values" )]
8+
[RoutePrefix( "api/values" )]
99
public class ValuesController : ApiController
1010
{
11-
public IHttpActionResult Get() => Ok( new { controller = GetType().Name, version = Request.GetRequestedApiVersion().ToString() } );
11+
[Route]
12+
public IHttpActionResult Get() =>
13+
Ok( new { controller = GetType().Name, version = Request.GetRequestedApiVersion().ToString() } );
14+
15+
[Route( "{id}" )]
16+
public IHttpActionResult Get( string id ) =>
17+
Ok( new { controller = GetType().Name, Id = id, version = Request.GetRequestedApiVersion().ToString() } );
1218
}

src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingMediaType/given a versioned ApiController/when using media type negotiation.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public async Task then_get_should_return_200( string controller, string apiVersi
3737
}
3838

3939
[Fact]
40-
public async Task then_get_should_return_415_for_an_unsupported_version()
40+
public async Task then_get_should_return_406_for_an_unsupported_version()
4141
{
4242
// arrange
4343
using var request = new HttpRequestMessage( Get, "api/values" )
@@ -49,6 +49,23 @@ public async Task then_get_should_return_415_for_an_unsupported_version()
4949
var response = await Client.SendAsync( request );
5050
var problem = await response.Content.ReadAsProblemDetailsAsync();
5151

52+
// assert
53+
response.StatusCode.Should().Be( NotAcceptable );
54+
problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type );
55+
}
56+
57+
[Fact]
58+
public async Task then_post_should_return_415_for_an_unsupported_version()
59+
{
60+
// arrange
61+
var entity = new { text = "Test" };
62+
var mediaType = Parse( "application/json;v=3.0" );
63+
using var content = new ObjectContent( entity.GetType(), entity, new JsonMediaTypeFormatter(), mediaType );
64+
65+
// act
66+
var response = await Client.PostAsync( "api/values", content );
67+
var problem = await response.Content.ReadAsProblemDetailsAsync();
68+
5269
// assert
5370
response.StatusCode.Should().Be( UnsupportedMediaType );
5471
problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type );

src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/HttpResponseExceptionFactory.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,15 +210,17 @@ private HttpResponseMessage CreateNotFound( ControllerSelectionResult convention
210210

211211
private HttpResponseMessage CreateUnsupportedMediaType()
212212
{
213+
var content = request.Content;
214+
var statusCode = content != null && content.Headers.ContentType != null ? UnsupportedMediaType : NotAcceptable;
213215
var version = request.GetRequestedApiVersion()?.ToString() ?? "(null)";
214216
var detail = string.Format( CultureInfo.CurrentCulture, SR.VersionedMediaTypeNotSupported, version );
215217

216218
TraceWriter.Info( request, ControllerSelectorCategory, detail );
217219

218220
var (type, title) = ProblemDetailsDefaults.Unsupported;
219-
var problem = ProblemDetails.CreateProblemDetails( request, (int) UnsupportedMediaType, title, type, detail );
221+
var problem = ProblemDetails.CreateProblemDetails( request, (int) statusCode, title, type, detail );
220222
var (mediaType, formatter) = request.GetProblemDetailsResponseType();
221223

222-
return request.CreateResponse( UnsupportedMediaType, problem, formatter, mediaType );
224+
return request.CreateResponse( statusCode, problem, formatter, mediaType );
223225
}
224226
}

src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingMediaType/Controllers/Values2Controller.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,17 @@ namespace Asp.Versioning.Mvc.UsingMediaType.Controllers;
1313
public class Values2Controller : ControllerBase
1414
{
1515
[HttpGet]
16-
public IActionResult Get( ApiVersion version ) => Ok( new { Controller = nameof( Values2Controller ), Version = version.ToString() } );
16+
public IActionResult Get( ApiVersion version ) =>
17+
Ok( new { Controller = nameof( Values2Controller ), Version = version.ToString() } );
18+
19+
[HttpGet( "{id}" )]
20+
public IActionResult Get( string id, ApiVersion version ) =>
21+
Ok( new { Controller = nameof( Values2Controller ), Id = id, Version = version.ToString() } );
22+
23+
[HttpPost]
24+
public IActionResult Post( JsonElement json ) => CreatedAtAction( nameof( Get ), new { id = "42" }, json );
1725

1826
[HttpPatch( "{id}" )]
1927
[Consumes( "application/merge-patch+json" )]
20-
public IActionResult MergePatch( JsonElement json ) => NoContent();
28+
public IActionResult MergePatch( string id, JsonElement json ) => NoContent();
2129
}

src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingMediaType/Controllers/ValuesController.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,10 @@ namespace Asp.Versioning.Mvc.UsingMediaType.Controllers;
1111
public class ValuesController : ControllerBase
1212
{
1313
[HttpGet]
14-
public IActionResult Get() => Ok( new { Controller = nameof( ValuesController ), Version = HttpContext.GetRequestedApiVersion().ToString() } );
14+
public IActionResult Get() =>
15+
Ok( new { Controller = nameof( ValuesController ), Version = HttpContext.GetRequestedApiVersion().ToString() } );
16+
17+
[HttpGet( "{id}" )]
18+
public IActionResult Get( string id ) =>
19+
Ok( new { Controller = nameof( ValuesController ), Id = id, Version = HttpContext.GetRequestedApiVersion().ToString() } );
1520
}

src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingMediaType/given a versioned Controller/when using media type negotiation.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public async Task then_get_should_return_200( string controller, string apiVersi
3838
}
3939

4040
[Fact]
41-
public async Task then_get_should_return_415_for_an_unsupported_version()
41+
public async Task then_get_should_return_406_for_an_unsupported_version()
4242
{
4343
// arrange
4444
using var request = new HttpRequestMessage( Get, "api/values" )
@@ -48,9 +48,29 @@ public async Task then_get_should_return_415_for_an_unsupported_version()
4848

4949
// act
5050
var response = await Client.SendAsync( request );
51+
var problem = await response.Content.ReadAsProblemDetailsAsync();
52+
53+
// assert
54+
response.StatusCode.Should().Be( NotAcceptable );
55+
problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type );
56+
}
57+
58+
[Fact]
59+
public async Task then_post_should_return_415_for_an_unsupported_version()
60+
{
61+
// arrange
62+
using var request = new HttpRequestMessage( Post, "api/values" )
63+
{
64+
Content = JsonContent.Create( new { test = true }, Parse( "application/json;v=3.0" ) ),
65+
};
66+
67+
// act
68+
var response = await Client.SendAsync( request );
69+
var problem = await response.Content.ReadAsProblemDetailsAsync();
5170

5271
// assert
5372
response.StatusCode.Should().Be( UnsupportedMediaType );
73+
problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type );
5474
}
5575

5676
[Fact]

src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ public PolicyJumpTable BuildJumpTable( int exitDestination, IReadOnlyList<Policy
139139
case EndpointType.AssumeDefault:
140140
rejection.AssumeDefault = edge.Destination;
141141
break;
142+
case EndpointType.NotAcceptable:
143+
rejection.NotAcceptable = edge.Destination;
144+
break;
142145
default:
143146
if ( versionsByUrl && state.RoutePatterns.Count > 0 )
144147
{

src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyJumpTable.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace Asp.Versioning.Routing;
55
using Microsoft.AspNetCore.Http;
66
using Microsoft.AspNetCore.Routing.Matching;
77
using Microsoft.AspNetCore.Routing.Patterns;
8+
using Microsoft.Net.Http.Headers;
89
using System.Runtime.CompilerServices;
910

1011
internal sealed class ApiVersionPolicyJumpTable : PolicyJumpTable
@@ -36,13 +37,14 @@ internal ApiVersionPolicyJumpTable(
3637

3738
public override int GetDestination( HttpContext httpContext )
3839
{
40+
var request = httpContext.Request;
3941
var feature = httpContext.ApiVersioningFeature();
4042
var apiVersions = new List<string>( capacity: feature.RawRequestedApiVersions.Count + 1 );
4143

4244
apiVersions.AddRange( feature.RawRequestedApiVersions );
4345

4446
if ( versionsByUrl &&
45-
TryGetApiVersionFromPath( httpContext.Request, out var rawApiVersion ) &&
47+
TryGetApiVersionFromPath( request, out var rawApiVersion ) &&
4648
DoesNotContainApiVersion( apiVersions, rawApiVersion ) )
4749
{
4850
apiVersions.Add( rawApiVersion );
@@ -86,9 +88,17 @@ public override int GetDestination( HttpContext httpContext )
8688
return destination;
8789
}
8890

89-
return versionsByMediaTypeOnly
90-
? rejection.UnsupportedMediaType // 415
91-
: rejection.Exit; // 404
91+
if ( versionsByMediaTypeOnly )
92+
{
93+
if ( request.Headers.ContainsKey( HeaderNames.ContentType ) )
94+
{
95+
return rejection.UnsupportedMediaType; // 415
96+
}
97+
98+
return rejection.NotAcceptable; // 406
99+
}
100+
101+
return rejection.Exit; // 404
92102
}
93103

94104
var addedFromUrl = apiVersions.Count == apiVersions.Capacity;

src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeBuilder.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@ public EdgeBuilder(
2828
unspecifiedNotAllowed = !options.AssumeDefaultVersionWhenUnspecified;
2929
constraintName = options.RouteConstraintName;
3030
keys = new( capacity + 1 );
31-
edges = new( capacity + 5 )
31+
edges = new( capacity + 6 )
3232
{
3333
[EdgeKey.Malformed] = new( capacity: 1 ) { new MalformedApiVersionEndpoint( logger ) },
3434
[EdgeKey.Ambiguous] = new( capacity: 1 ) { new AmbiguousApiVersionEndpoint( logger ) },
3535
[EdgeKey.Unspecified] = new( capacity: 1 ) { new UnspecifiedApiVersionEndpoint( logger ) },
3636
[EdgeKey.UnsupportedMediaType] = new( capacity: 1 ) { new UnsupportedMediaTypeEndpoint() },
37+
[EdgeKey.NotAcceptable] = new( capacity: 1 ) { new NotAcceptableEndpoint() },
3738
};
3839
}
3940

src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeKey.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ internal EdgeKey( ApiVersion apiVersion )
3434

3535
internal static EdgeKey UnsupportedMediaType => new( EndpointType.UnsupportedMediaType, new( capacity: 0 ) );
3636

37+
internal static EdgeKey NotAcceptable => new( EndpointType.NotAcceptable, new( capacity: 0 ) );
38+
3739
internal static EdgeKey AssumeDefault => new( EndpointType.AssumeDefault, new() );
3840

3941
public bool Equals( [AllowNull] EdgeKey other ) => GetHashCode() == other.GetHashCode();
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace Asp.Versioning.Routing;
4+
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Http.Extensions;
7+
using Microsoft.Extensions.DependencyInjection;
8+
using System.Globalization;
9+
10+
internal static class EndpointProblem
11+
{
12+
internal static Task UnsupportedApiVersion( HttpContext context, int statusCode )
13+
{
14+
var services = context.RequestServices;
15+
var factory = services.GetRequiredService<IProblemDetailsFactory>();
16+
var url = new Uri( context.Request.GetDisplayUrl() ).SafeFullPath();
17+
var apiVersion = context.ApiVersioningFeature().RawRequestedApiVersion;
18+
var (type, title) = ProblemDetailsDefaults.Unsupported;
19+
var detail = string.Format(
20+
CultureInfo.CurrentCulture,
21+
SR.VersionedResourceNotSupported,
22+
url,
23+
apiVersion );
24+
var problem = factory.CreateProblemDetails(
25+
context.Request,
26+
statusCode,
27+
title,
28+
type,
29+
detail );
30+
31+
context.Response.StatusCode = statusCode;
32+
33+
return context.Response.WriteAsJsonAsync(
34+
problem,
35+
options: default,
36+
contentType: ProblemDetailsDefaults.MediaType.Json );
37+
}
38+
}

src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointType.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ internal enum EndpointType
1010
Unspecified,
1111
UnsupportedMediaType,
1212
AssumeDefault,
13+
NotAcceptable,
1314
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace Asp.Versioning.Routing;
4+
5+
using Microsoft.AspNetCore.Http;
6+
using static Microsoft.AspNetCore.Http.EndpointMetadataCollection;
7+
8+
internal sealed class NotAcceptableEndpoint : Endpoint
9+
{
10+
private const string Name = "406 HTTP Not Acceptable";
11+
12+
internal NotAcceptableEndpoint() : base( OnExecute, Empty, Name ) { }
13+
14+
private static Task OnExecute( HttpContext context ) =>
15+
EndpointProblem.UnsupportedApiVersion( context, StatusCodes.Status406NotAcceptable );
16+
}

src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/RouteDestination.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ internal struct RouteDestination
1010
public int Unspecified;
1111
public int UnsupportedMediaType;
1212
public int AssumeDefault;
13+
public int NotAcceptable;
1314

1415
public RouteDestination( int exit )
1516
{
@@ -19,5 +20,6 @@ public RouteDestination( int exit )
1920
Unspecified = exit;
2021
UnsupportedMediaType = exit;
2122
AssumeDefault = exit;
23+
NotAcceptable = exit;
2224
}
2325
}

src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnsupportedApiVersionEndpoint.cs

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33
namespace Asp.Versioning.Routing;
44

55
using Microsoft.AspNetCore.Http;
6-
using Microsoft.AspNetCore.Http.Extensions;
7-
using Microsoft.Extensions.DependencyInjection;
8-
using System.Globalization;
96
using static Microsoft.AspNetCore.Http.EndpointMetadataCollection;
107

118
internal sealed class UnsupportedApiVersionEndpoint : Endpoint
@@ -14,30 +11,6 @@ internal sealed class UnsupportedApiVersionEndpoint : Endpoint
1411

1512
internal UnsupportedApiVersionEndpoint() : base( OnExecute, Empty, Name ) { }
1613

17-
private static Task OnExecute( HttpContext context )
18-
{
19-
var services = context.RequestServices;
20-
var factory = services.GetRequiredService<IProblemDetailsFactory>();
21-
var url = new Uri( context.Request.GetDisplayUrl() ).SafeFullPath();
22-
var apiVersion = context.ApiVersioningFeature().RawRequestedApiVersion;
23-
var (type, title) = ProblemDetailsDefaults.Unsupported;
24-
var detail = string.Format(
25-
CultureInfo.CurrentCulture,
26-
SR.VersionedResourceNotSupported,
27-
url,
28-
apiVersion );
29-
var problem = factory.CreateProblemDetails(
30-
context.Request,
31-
StatusCodes.Status400BadRequest,
32-
title,
33-
type,
34-
detail );
35-
36-
context.Response.StatusCode = StatusCodes.Status400BadRequest;
37-
38-
return context.Response.WriteAsJsonAsync(
39-
problem,
40-
options: default,
41-
contentType: ProblemDetailsDefaults.MediaType.Json );
42-
}
14+
private static Task OnExecute( HttpContext context ) =>
15+
EndpointProblem.UnsupportedApiVersion( context, StatusCodes.Status400BadRequest );
4316
}

src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnsupportedMediaTypeEndpoint.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ internal sealed class UnsupportedMediaTypeEndpoint : Endpoint
1111

1212
internal UnsupportedMediaTypeEndpoint() : base( OnExecute, Empty, Name ) { }
1313

14-
private static Task OnExecute( HttpContext context )
15-
{
16-
context.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
17-
return Task.CompletedTask;
18-
}
14+
private static Task OnExecute( HttpContext context ) =>
15+
EndpointProblem.UnsupportedApiVersion( context, StatusCodes.Status415UnsupportedMediaType );
1916
}

0 commit comments

Comments
 (0)