Skip to content

Commit 368dd5c

Browse files
Report API versions when unspecified or malformed. Fixes #1120
1 parent 3660a24 commit 368dd5c

File tree

10 files changed

+102
-22
lines changed

10 files changed

+102
-22
lines changed

src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/MinimalApiFixture.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ protected override void OnConfigureEndpoints( IEndpointRouteBuilder endpoints )
5757

5858
protected override void OnAddApiVersioning( ApiVersioningOptions options )
5959
{
60+
options.ReportApiVersions = true;
6061
options.ApiVersionReader = ApiVersionReader.Combine(
6162
new QueryStringApiVersionReader(),
6263
new UrlSegmentApiVersionReader(),

src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/given a versioned minimal API/when using a query string.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ namespace given_a_versioned_minimal_API;
44

55
using Asp.Versioning;
66
using Asp.Versioning.Http;
7+
using static System.Net.HttpStatusCode;
78

89
[Collection( nameof( MinimalApiTestCollection ) )]
910
public class when_using_a_query_string : AcceptanceTest
@@ -34,9 +35,58 @@ public async Task then_get_should_report_api_versions()
3435
var response = await GetAsync( "api/values?api-version=1.0" );
3536

3637
// assert
38+
response.StatusCode.Should().Be( OK );
3739
response.Headers.GetValues( "api-supported-versions" ).Should().Equal( "1.0, 2.0" );
3840
}
3941

42+
[Fact]
43+
public async Task then_get_should_return_400_for_an_unsupported_version()
44+
{
45+
// arrange
46+
47+
48+
// act
49+
var response = await GetAsync( "api/values?api-version=3.0" );
50+
var problem = await response.Content.ReadAsProblemDetailsAsync();
51+
52+
// assert
53+
response.StatusCode.Should().Be( BadRequest );
54+
response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0" );
55+
problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type );
56+
}
57+
58+
[Fact]
59+
public async Task then_get_should_return_400_for_an_unspecified_version()
60+
{
61+
// arrange
62+
63+
64+
// act
65+
var response = await GetAsync( "api/values" );
66+
var problem = await response.Content.ReadAsProblemDetailsAsync();
67+
68+
// assert
69+
response.StatusCode.Should().Be( BadRequest );
70+
response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0" );
71+
problem.Type.Should().Be( ProblemDetailsDefaults.Unspecified.Type );
72+
}
73+
74+
[Fact]
75+
public async Task then_get_should_return_400_for_a_malformed_version()
76+
{
77+
// arrange
78+
79+
80+
// act
81+
var response = await GetAsync( "api/values?api-version=abc" );
82+
var problem = await response.Content.ReadAsProblemDetailsAsync();
83+
84+
// assert
85+
response.StatusCode.Should().Be( BadRequest );
86+
response.Headers.GetValues( "api-supported-versions" ).Should().Equal( "1.0, 2.0" );
87+
problem.Type.Should().Be( ProblemDetailsDefaults.Invalid.Type );
88+
}
89+
4090
public when_using_a_query_string( MinimalApiFixture fixture, ITestOutputHelper console )
4191
: base( fixture ) => console.WriteLine( fixture.DirectedGraphVisualizationUrl );
4292
}

src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/given a versioned Controller/when using a query string and split into two types.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,12 @@ public async Task then_get_should_return_400_for_an_unsupported_version()
9494

9595
// act
9696
var response = await GetAsync( "api/values?api-version=3.0" );
97+
var problem = await response.Content.ReadAsProblemDetailsAsync();
9798

9899
// assert
99100
response.StatusCode.Should().Be( BadRequest );
101+
response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0" );
102+
problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type );
100103
}
101104

102105
[Fact]
@@ -111,6 +114,7 @@ public async Task then_get_should_return_400_for_an_unspecified_version()
111114

112115
// assert
113116
response.StatusCode.Should().Be( BadRequest );
117+
response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0" );
114118
problem.Type.Should().Be( ProblemDetailsDefaults.Unspecified.Type );
115119
}
116120

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,14 +337,14 @@ private static void Collate(
337337
{
338338
// this is a best guess effort at collating all supported and deprecated
339339
// versions for an api when unmatched and it needs to be reported. it's
340-
// impossible to sure as there is no way to correlate an arbitrary
340+
// impossible to be sure as there is no way to correlate an arbitrary
341341
// request url by endpoint or name. the routing system will build a tree
342342
// based on the route template before the jump table policy is created,
343343
// which provides a natural method of grouping. manual, contrived tests
344344
// demonstrated that were the results were correctly collated together.
345345
// it is possible there is an edge case that isn't covered, but it's
346346
// unclear what that would look like. one or more test cases should be
347-
// added to document that if discovered
347+
// added to document that is discovered
348348
ApiVersionModel model;
349349

350350
if ( supported == null )

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ public override int GetDestination( HttpContext httpContext )
7676
return rejection.AssumeDefault;
7777
}
7878

79+
httpContext.Features.Set( policyFeature );
80+
7981
// 3. unspecified
8082
return versionsByUrlOnly
8183
/* 404 */ ? rejection.Exit
@@ -86,6 +88,8 @@ public override int GetDestination( HttpContext httpContext )
8688

8789
if ( !parser.TryParse( rawApiVersion, out var apiVersion ) )
8890
{
91+
httpContext.Features.Set( policyFeature );
92+
8993
if ( versionsByUrl )
9094
{
9195
feature.RawRequestedApiVersion = rawApiVersion;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public Endpoint Build()
3030
{
3131
if ( feature.RawRequestedApiVersions.Count == 0 )
3232
{
33-
return new UnspecifiedApiVersionEndpoint( logger, GetDisplayNames() );
33+
return new UnspecifiedApiVersionEndpoint( logger, options, GetDisplayNames() );
3434
}
3535

3636
return new UnsupportedApiVersionEndpoint( options );

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ public EdgeBuilder(
3232
keys = new( capacity + 1 );
3333
edges = new( capacity + RejectionEndpointCapacity )
3434
{
35-
[EdgeKey.Malformed] = new( capacity: 1 ) { new MalformedApiVersionEndpoint( logger ) },
36-
[EdgeKey.Ambiguous] = new( capacity: 1 ) { new AmbiguousApiVersionEndpoint( logger ) },
37-
[EdgeKey.Unspecified] = new( capacity: 1 ) { new UnspecifiedApiVersionEndpoint( logger ) },
38-
[EdgeKey.Unsupported] = new( capacity: 1 ) { new UnsupportedApiVersionEndpoint( options ) },
39-
[EdgeKey.UnsupportedMediaType] = new( capacity: 1 ) { new UnsupportedMediaTypeEndpoint( options ) },
40-
[EdgeKey.NotAcceptable] = new( capacity: 1 ) { new NotAcceptableEndpoint( options ) },
35+
[EdgeKey.Malformed] = [new MalformedApiVersionEndpoint( logger, options )],
36+
[EdgeKey.Ambiguous] = [new AmbiguousApiVersionEndpoint( logger )],
37+
[EdgeKey.Unspecified] = [new UnspecifiedApiVersionEndpoint( logger, options )],
38+
[EdgeKey.Unsupported] = [new UnsupportedApiVersionEndpoint( options )],
39+
[EdgeKey.UnsupportedMediaType] = [new UnsupportedMediaTypeEndpoint( options )],
40+
[EdgeKey.NotAcceptable] = [new NotAcceptableEndpoint( options )],
4141
};
4242
}
4343

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,30 @@ internal static ProblemDetailsContext New( HttpContext context, ProblemDetailsIn
3333
return newContext;
3434
}
3535

36-
internal static Task UnsupportedApiVersion(
37-
HttpContext context,
38-
ApiVersioningOptions options,
39-
int statusCode )
36+
internal static bool TryReportApiVersions( HttpContext context, ApiVersioningOptions options )
4037
{
41-
context.Response.StatusCode = statusCode;
42-
4338
if ( options.ReportApiVersions &&
4439
context.Features.Get<ApiVersionPolicyFeature>() is ApiVersionPolicyFeature feature )
4540
{
4641
var reporter = context.RequestServices.GetRequiredService<IReportApiVersions>();
4742
var model = feature.Metadata.Map( reporter.Mapping );
4843
context.Response.OnStarting( ReportApiVersions, (reporter, context.Response, model) );
44+
return true;
45+
}
46+
else
47+
{
48+
return false;
4949
}
50+
}
51+
52+
internal static Task UnsupportedApiVersion(
53+
HttpContext context,
54+
ApiVersioningOptions options,
55+
int statusCode )
56+
{
57+
context.Response.StatusCode = statusCode;
58+
59+
TryReportApiVersions( context, options );
5060

5161
if ( context.TryGetProblemDetailsService( out var problemDetails ) )
5262
{

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,18 @@ internal sealed class MalformedApiVersionEndpoint : Endpoint
1212
{
1313
private const string Name = "400 Invalid API Version";
1414

15-
internal MalformedApiVersionEndpoint( ILogger logger )
16-
: base( c => OnExecute( c, logger ), Empty, Name ) { }
15+
internal MalformedApiVersionEndpoint( ILogger logger, ApiVersioningOptions options )
16+
: base( context => OnExecute( context, options, logger ), Empty, Name ) { }
1717

18-
private static Task OnExecute( HttpContext context, ILogger logger )
18+
private static Task OnExecute( HttpContext context, ApiVersioningOptions options, ILogger logger )
1919
{
2020
var requestedVersion = context.ApiVersioningFeature().RawRequestedApiVersion;
2121

2222
logger.ApiVersionInvalid( requestedVersion );
2323
context.Response.StatusCode = StatusCodes.Status400BadRequest;
2424

25+
EndpointProblem.TryReportApiVersions( context, options );
26+
2527
if ( !context.TryGetProblemDetailsService( out var problemDetails ) )
2628
{
2729
return Task.CompletedTask;

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,17 @@ internal sealed class UnspecifiedApiVersionEndpoint : Endpoint
1010
{
1111
private const string Name = "400 Unspecified API Version";
1212

13-
internal UnspecifiedApiVersionEndpoint( ILogger logger, string[]? displayNames = default )
14-
: base( c => OnExecute( c, displayNames, logger ), Empty, Name ) { }
15-
16-
private static Task OnExecute( HttpContext context, string[]? candidateEndpoints, ILogger logger )
13+
internal UnspecifiedApiVersionEndpoint(
14+
ILogger logger,
15+
ApiVersioningOptions options,
16+
string[]? displayNames = default )
17+
: base( context => OnExecute( context, options, displayNames, logger ), Empty, Name ) { }
18+
19+
private static Task OnExecute(
20+
HttpContext context,
21+
ApiVersioningOptions options,
22+
string[]? candidateEndpoints,
23+
ILogger logger )
1724
{
1825
if ( candidateEndpoints == null || candidateEndpoints.Length == 0 )
1926
{
@@ -26,6 +33,8 @@ private static Task OnExecute( HttpContext context, string[]? candidateEndpoints
2633

2734
context.Response.StatusCode = StatusCodes.Status400BadRequest;
2835

36+
EndpointProblem.TryReportApiVersions( context, options );
37+
2938
if ( context.TryGetProblemDetailsService( out var problemDetails ) )
3039
{
3140
return problemDetails.TryWriteAsync(

0 commit comments

Comments
 (0)