|
10 | 10 | using System;
|
11 | 11 | using System.Collections.Generic;
|
12 | 12 | using System.Linq;
|
| 13 | + using System.Text.RegularExpressions; |
13 | 14 | using System.Threading.Tasks;
|
14 | 15 | using static Microsoft.AspNetCore.Mvc.Versioning.ApiVersionMapping;
|
15 | 16 | using static Microsoft.AspNetCore.Mvc.Versioning.ErrorCodes;
|
@@ -98,7 +99,7 @@ public Task ApplyAsync( HttpContext httpContext, CandidateSet candidates )
|
98 | 99 |
|
99 | 100 | var (matched, hasCandidates) = MatchApiVersion( candidates, apiVersion );
|
100 | 101 |
|
101 |
| - if ( !matched && hasCandidates ) |
| 102 | + if ( !matched && hasCandidates && !DifferByRouteConstraintsOnly( candidates ) ) |
102 | 103 | {
|
103 | 104 | httpContext.SetEndpoint( ClientError( httpContext, candidates ) );
|
104 | 105 | }
|
@@ -184,6 +185,66 @@ public Task ApplyAsync( HttpContext httpContext, CandidateSet candidates )
|
184 | 185 | return (true, hasCandidates);
|
185 | 186 | }
|
186 | 187 |
|
| 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 | + |
187 | 248 | bool IsRequestedApiVersionAmbiguous( HttpContext httpContext, out ApiVersion? apiVersion )
|
188 | 249 | {
|
189 | 250 | try
|
|
0 commit comments