Skip to content

Commit 01feecb

Browse files
Handle candidate difference by route constraint only. Fixes #797
1 parent 861a9ee commit 01feecb

File tree

1 file changed

+62
-1
lines changed

1 file changed

+62
-1
lines changed

src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ApiVersionMatcherPolicy.cs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System;
1111
using System.Collections.Generic;
1212
using System.Linq;
13+
using System.Text.RegularExpressions;
1314
using System.Threading.Tasks;
1415
using static Microsoft.AspNetCore.Mvc.Versioning.ApiVersionMapping;
1516
using static Microsoft.AspNetCore.Mvc.Versioning.ErrorCodes;
@@ -98,7 +99,7 @@ public Task ApplyAsync( HttpContext httpContext, CandidateSet candidates )
9899

99100
var (matched, hasCandidates) = MatchApiVersion( candidates, apiVersion );
100101

101-
if ( !matched && hasCandidates )
102+
if ( !matched && hasCandidates && !DifferByRouteConstraintsOnly( candidates ) )
102103
{
103104
httpContext.SetEndpoint( ClientError( httpContext, candidates ) );
104105
}
@@ -184,6 +185,66 @@ public Task ApplyAsync( HttpContext httpContext, CandidateSet candidates )
184185
return (true, hasCandidates);
185186
}
186187

188+
static bool DifferByRouteConstraintsOnly( CandidateSet candidates )
189+
{
190+
if ( candidates.Count < 2 )
191+
{
192+
return false;
193+
}
194+
195+
// HACK: edge case where the only differences are route template semantics.
196+
// the established behavior is 400 when an endpoint 'could' match, but doesn't.
197+
// this will not work for the scenario:
198+
//
199+
// * 1.0 = values/{id}
200+
// * 2.0 = values/{id:int}
201+
//
202+
// Where the requested version is 2.0 and {id} is 'abc'. Users expect 404 in this
203+
// scenario. Both candidates have been eliminated, but the policy doesn't know why.
204+
// the only differences are route constraints; otherwise, the templates are equivalent.
205+
//
206+
// for the scenario:
207+
//
208+
// * 1.0 = values/{id}
209+
// * 2.0 = values/{id}
210+
//
211+
// but 3.0 is requested, 400 should be returned if we made it this far
212+
const string ReplacementPattern = "{$1}";
213+
var pattern = new Regex( "{([^:]+):[^}]+}", RegexOptions.Singleline | RegexOptions.IgnoreCase );
214+
var comparer = StringComparer.OrdinalIgnoreCase;
215+
string? template = default;
216+
string? normalizedTemplate = default;
217+
218+
for ( var i = 0; i < candidates.Count; i++ )
219+
{
220+
ref var candidate = ref candidates[i];
221+
222+
if ( candidate.Endpoint is not RouteEndpoint endpoint )
223+
{
224+
return false;
225+
}
226+
227+
var otherTemplate = endpoint.RoutePattern.RawText ?? string.Empty;
228+
229+
if ( template is null )
230+
{
231+
template = otherTemplate;
232+
normalizedTemplate = pattern.Replace( otherTemplate, ReplacementPattern );
233+
}
234+
else if ( !comparer.Equals( template, otherTemplate ) )
235+
{
236+
var normalizedOtherTemplate = pattern.Replace( otherTemplate, ReplacementPattern );
237+
238+
if ( comparer.Equals( normalizedTemplate, normalizedOtherTemplate ) )
239+
{
240+
return true;
241+
}
242+
}
243+
}
244+
245+
return false;
246+
}
247+
187248
bool IsRequestedApiVersionAmbiguous( HttpContext httpContext, out ApiVersion? apiVersion )
188249
{
189250
try

0 commit comments

Comments
 (0)