Skip to content

Commit 239ec61

Browse files
Updates 400 responses to use the standard error response format with support to change the format. Fixes #26 (#29)
1 parent 6464a49 commit 239ec61

File tree

10 files changed

+323
-38
lines changed

10 files changed

+323
-38
lines changed

src/Common/Versioning/ApiVersioningOptions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Mvc.Versioning
1111
/// <summary>
1212
/// Represents the possible API versioning options for services.
1313
/// </summary>
14-
public class ApiVersioningOptions
14+
public partial class ApiVersioningOptions
1515
{
1616
private ApiVersion defaultApiVersion = ApiVersion.Default;
1717
private IApiVersionReader apiVersionReader = new QueryStringApiVersionReader();

src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ApiVersionControllerSelector.cs

+3-4
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
using System.Web.Http.Dispatcher;
1414
using Versioning;
1515
using static Controllers.HttpControllerDescriptorComparer;
16-
using static System.Net.HttpStatusCode;
1716
using static System.StringComparer;
1817

1918
/// <summary>
@@ -165,9 +164,9 @@ private static void EnsureRequestHasValidApiVersion( HttpRequestMessage request
165164
}
166165
catch ( AmbiguousApiVersionException ex )
167166
{
168-
var error = new HttpError( ex.Message ) { ["Code"] = "AmbiguousApiVersion" };
169-
throw new HttpResponseException( request.CreateErrorResponse( BadRequest, error ) );
167+
var options = request.GetApiVersioningOptions();
168+
throw new HttpResponseException( options.CreateBadRequest( request, "AmbiguousApiVersion", ex.Message, null ) );
170169
}
171170
}
172171
}
173-
}
172+
}

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

+28-11
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Web.Http;
77
using System.Web.Http.Dispatcher;
88
using System.Web.Http.Tracing;
9+
using Versioning;
910
using static ApiVersion;
1011
using static System.Net.HttpStatusCode;
1112

@@ -23,10 +24,35 @@ internal HttpResponseExceptionFactory( HttpRequestMessage request )
2324

2425
private ITraceWriter TraceWriter => request.GetConfiguration().Services.GetTraceWriter() ?? NullTraceWriter.Instance;
2526

27+
private ApiVersioningOptions Options => request.GetApiVersioningOptions();
28+
2629
[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created exception cannot be disposed. Handled by the caller." )]
2730
internal HttpResponseException NewNotFoundOrBadRequestException( ControllerSelectionResult conventionRouteResult, ControllerSelectionResult directRouteResult ) =>
2831
CreateBadRequestForUnsupportedApiVersion( conventionRouteResult, directRouteResult ) ?? CreateBadRequestForInvalidApiVersion() ?? CreateNotFound( conventionRouteResult );
2932

33+
[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created exception cannot be disposed. Handled by the caller." )]
34+
internal HttpResponseMessage CreateBadRequestResponseForUnsupportedApiVersion( ApiVersion requestedVersion )
35+
{
36+
Contract.Requires( requestedVersion != null );
37+
Contract.Ensures( Contract.Result<HttpResponseMessage>() != null );
38+
39+
var message = SR.VersionedResourceNotSupported.FormatDefault( request.RequestUri, requestedVersion );
40+
var messageDetail = SR.VersionedControllerNameNotFound.FormatDefault( request.RequestUri, requestedVersion );
41+
42+
TraceWriter.Info( request, ControllerSelectorCategory, message );
43+
44+
return Options.CreateBadRequest( request, "UnsupportedApiVersion", message, messageDetail );
45+
}
46+
47+
[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created exception cannot be disposed. Handled by the caller." )]
48+
internal HttpResponseException CreateBadRequestForUnsupportedApiVersion( ApiVersion requestedVersion )
49+
{
50+
Contract.Requires( requestedVersion != null );
51+
Contract.Ensures( Contract.Result<HttpResponseException>() != null );
52+
53+
return new HttpResponseException( CreateBadRequestResponseForUnsupportedApiVersion( requestedVersion ) );
54+
}
55+
3056
[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created exception cannot be disposed. Handled by the caller." )]
3157
private HttpResponseException CreateBadRequestForUnsupportedApiVersion( ControllerSelectionResult conventionRouteResult, ControllerSelectionResult directRouteResult )
3258
{
@@ -47,14 +73,7 @@ private HttpResponseException CreateBadRequestForUnsupportedApiVersion( Controll
4773
return null;
4874
}
4975

50-
var message = SR.VersionedResourceNotSupported.FormatDefault( request.RequestUri, requestedVersion );
51-
var messageDetail = SR.VersionedControllerNameNotFound.FormatDefault( request.RequestUri, requestedVersion );
52-
var error = new HttpError() { Message = message, MessageDetail = messageDetail };
53-
54-
error["Code"] = "UnsupportedApiVersion";
55-
TraceWriter.Info( request, ControllerSelectorCategory, message );
56-
57-
return new HttpResponseException( request.CreateErrorResponse( BadRequest, error ) );
76+
return CreateBadRequestForUnsupportedApiVersion( requestedVersion );
5877
}
5978

6079
[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created exception cannot be disposed. Handled by the caller." )]
@@ -70,12 +89,10 @@ private HttpResponseException CreateBadRequestForInvalidApiVersion()
7089

7190
var message = SR.VersionedResourceNotSupported.FormatDefault( request.RequestUri, requestedVersion );
7291
var messageDetail = SR.VersionedControllerNameNotFound.FormatDefault( request.RequestUri, requestedVersion );
73-
var error = new HttpError() { Message = message, MessageDetail = messageDetail };
7492

75-
error["Code"] = "InvalidApiVersion";
7693
TraceWriter.Info( request, ControllerSelectorCategory, message );
7794

78-
return new HttpResponseException( request.CreateErrorResponse( BadRequest, error ) );
95+
return new HttpResponseException( Options.CreateBadRequest( request, "InvalidApiVersion", message, messageDetail ) );
7996
}
8097

8198
[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created exception cannot be disposed. Handled by the caller." )]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
namespace Microsoft.Web.Http.Versioning
2+
{
3+
using System.Diagnostics.Contracts;
4+
using System.Net.Http;
5+
using System.Net.Http.Formatting;
6+
using System.Web.Http;
7+
using static System.Net.HttpStatusCode;
8+
using static System.String;
9+
10+
/// <content>
11+
/// Provides additional implementation specific to Microsoft ASP.NET Web API.
12+
/// </content>
13+
public partial class ApiVersioningOptions
14+
{
15+
private CreateBadRequestDelegate createBadRequest = CreateDefaultBadRequest;
16+
17+
/// <summary>
18+
/// Gets or sets the function to used to create HTTP 400 (Bad Request) responses related to API versioning.
19+
/// </summary>
20+
/// <value>The <see cref="CreateBadRequestDelegate">function</see> to used to create a HTTP 400 (Bad Request)
21+
/// <see cref="HttpResponseMessage">response</see> related to API versioning.</value>
22+
/// <remarks>The default value generates responses that are compliant with the Microsoft REST API Guidelines.
23+
/// This option should only be changed by service authors that intentionally want to deviate from the
24+
/// established guidance.</remarks>
25+
public CreateBadRequestDelegate CreateBadRequest
26+
{
27+
get
28+
{
29+
Contract.Ensures( createBadRequest != null );
30+
return createBadRequest;
31+
}
32+
set
33+
{
34+
Arg.NotNull( value, nameof( value ) );
35+
createBadRequest = value;
36+
}
37+
}
38+
39+
private static HttpResponseMessage CreateDefaultBadRequest( HttpRequestMessage request, string code, string message, string messageDetail )
40+
{
41+
if ( request == null || !IsODataRequest( request ) )
42+
{
43+
return CreateWebApiBadRequest( request, code, message, messageDetail );
44+
}
45+
46+
return CreateODataBadRequest( request, code, message, messageDetail );
47+
}
48+
49+
private static HttpResponseMessage CreateWebApiBadRequest( HttpRequestMessage request, string code, string message, string messageDetail )
50+
{
51+
var error = new HttpError();
52+
var root = new HttpError() { ["Error"] = error };
53+
54+
if ( !IsNullOrEmpty( code ) )
55+
{
56+
error["Code"] = code;
57+
}
58+
59+
if ( !IsNullOrEmpty( message ) )
60+
{
61+
error.Message = message;
62+
}
63+
64+
if ( !IsNullOrEmpty( messageDetail ) && request?.ShouldIncludeErrorDetail() == true )
65+
{
66+
error["InnerError"] = new HttpError( messageDetail );
67+
}
68+
69+
if ( request == null )
70+
{
71+
return new HttpResponseMessage( BadRequest )
72+
{
73+
Content = new ObjectContent<HttpError>( root, new JsonMediaTypeFormatter() )
74+
};
75+
}
76+
77+
return request.CreateErrorResponse( BadRequest, root );
78+
}
79+
80+
private static bool IsODataRequest( HttpRequestMessage request )
81+
{
82+
if ( request == null )
83+
{
84+
return false;
85+
}
86+
87+
var routeValues = request.GetRouteData();
88+
89+
if ( routeValues == null )
90+
{
91+
return false;
92+
}
93+
94+
if ( !routeValues.Values.ContainsKey( "odataPath" ) )
95+
{
96+
return false;
97+
}
98+
99+
return request.GetConfiguration()?.Formatters.JsonFormatter == null;
100+
}
101+
102+
private static HttpResponseMessage CreateODataBadRequest( HttpRequestMessage request, string code, string message, string messageDetail )
103+
{
104+
Contract.Requires( request != null );
105+
106+
var error = new HttpError();
107+
108+
if ( !IsNullOrEmpty( code ) )
109+
{
110+
error[HttpErrorKeys.ErrorCodeKey] = code;
111+
}
112+
113+
if ( !IsNullOrEmpty( message ) )
114+
{
115+
error.Message = message;
116+
}
117+
118+
if ( !IsNullOrEmpty( messageDetail ) && request?.ShouldIncludeErrorDetail() == true )
119+
{
120+
error[HttpErrorKeys.MessageDetailKey] = messageDetail;
121+
}
122+
123+
return request.CreateErrorResponse( BadRequest, error );
124+
}
125+
}
126+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace Microsoft.Web.Http.Versioning
2+
{
3+
using System;
4+
using System.Net.Http;
5+
using System.Web.Http;
6+
7+
/// <summary>
8+
/// Represents the function invoked to create a HTTP 400 (Bad Request) response related to API versioning.
9+
/// </summary>
10+
/// <param name="request">The current <see cref="HttpRequestMessage">HTTP request</see>.</param>
11+
/// <param name="code">The associated error code.</param>
12+
/// <param name="message">The error message.</param>
13+
/// <param name="messageDetail">The detailed error message, if any.</param>
14+
/// <returns>A <see cref="HttpResponseMessage">HTTP response</see> representing for status code 400 (Bad Request).</returns>
15+
public delegate HttpResponseMessage CreateBadRequestDelegate( HttpRequestMessage request, string code, string message, string messageDetail );
16+
}

src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionActionSelector.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ private BadRequestHandler VerifyRequestedApiVersionIsNotAmbiguous( HttpContext h
203203
{
204204
logger.LogInformation( ex.Message );
205205
apiVersion = default( ApiVersion );
206-
return new BadRequestHandler( "AmbiguousApiVersion", ex.Message );
206+
return new BadRequestHandler( Options, "AmbiguousApiVersion", ex.Message );
207207
}
208208

209209
return null;
@@ -251,7 +251,7 @@ private BadRequestHandler IsValidRequest( ActionSelectionContext context )
251251
}
252252

253253
var message = SR.VersionedResourceNotSupported.FormatDefault( context.HttpContext.Request.GetDisplayUrl(), requestedVersion );
254-
return new BadRequestHandler( code, message );
254+
return new BadRequestHandler( Options, code, message );
255255
}
256256

257257
private static IEnumerable<ActionDescriptor> MatchVersionNeutralActions( ActionSelectionContext context ) =>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
namespace Microsoft.AspNetCore.Mvc.Versioning
2+
{
3+
using Hosting;
4+
using Http;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Diagnostics.Contracts;
8+
using static System.String;
9+
10+
/// <content>
11+
/// Provides additional implementation specific to Microsoft ASP.NET Web API.
12+
/// </content>
13+
public partial class ApiVersioningOptions
14+
{
15+
private CreateBadRequestDelegate createBadRequest = CreateDefaultBadRequest;
16+
17+
/// <summary>
18+
/// Gets or sets the function to used to create HTTP 400 (Bad Request) responses related to API versioning.
19+
/// </summary>
20+
/// <value>The <see cref="CreateBadRequestDelegate">function</see> to used to create a HTTP 400 (Bad Request)
21+
/// <see cref="HttpResponse">response</see> related to API versioning.</value>
22+
/// <remarks>The default value generates responses that are compliant with the Microsoft REST API Guidelines.
23+
/// This option should only be changed by service authors that intentionally want to deviate from the
24+
/// established guidance.</remarks>
25+
[CLSCompliant( false )]
26+
public CreateBadRequestDelegate CreateBadRequest
27+
{
28+
get
29+
{
30+
Contract.Ensures( createBadRequest != null );
31+
return createBadRequest;
32+
}
33+
set
34+
{
35+
Arg.NotNull( value, nameof( value ) );
36+
createBadRequest = value;
37+
}
38+
}
39+
40+
private static BadRequestObjectResult CreateDefaultBadRequest( HttpRequest request, string code, string message, string messageDetail )
41+
{
42+
var error = new Dictionary<string, object>();
43+
var root = new Dictionary<string, object>() { ["Error"] = error };
44+
45+
if ( !IsNullOrEmpty( code ) )
46+
{
47+
error["Code"] = code;
48+
}
49+
50+
if ( !IsNullOrEmpty( message ) )
51+
{
52+
error["Message"] = message;
53+
}
54+
55+
if ( !IsNullOrEmpty( messageDetail ) )
56+
{
57+
var environment = (IHostingEnvironment) request?.HttpContext.RequestServices.GetService( typeof( IHostingEnvironment ) );
58+
59+
if ( environment?.IsDevelopment() == true )
60+
{
61+
error["InnerError"] = new Dictionary<string, object>() { ["Message"] = messageDetail };
62+
}
63+
}
64+
65+
return new BadRequestObjectResult( root );
66+
}
67+
}
68+
}

src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/BadRequestHandler.cs

+9-5
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,21 @@
88

99
internal sealed class BadRequestHandler
1010
{
11+
private readonly ApiVersioningOptions options;
1112
private readonly string code;
1213
private readonly string message;
1314

14-
internal BadRequestHandler( string message )
15-
: this( null, message )
15+
internal BadRequestHandler( ApiVersioningOptions options, string message )
16+
: this( options, null, message )
1617
{
1718
}
1819

19-
internal BadRequestHandler( string code, string message )
20+
internal BadRequestHandler( ApiVersioningOptions options, string code, string message )
2021
{
22+
Contract.Requires( options != null );
2123
Contract.Requires( !string.IsNullOrEmpty( message ) );
24+
25+
this.options = options;
2226
this.message = message;
2327
this.code = code;
2428
}
@@ -33,10 +37,10 @@ internal async Task ExecuteAsync( HttpContext context )
3337
RouteData = context.GetRouteData(),
3438
ActionDescriptor = new ActionDescriptor()
3539
};
36-
var result = new BadRequestObjectResult( new { Code = code, Message = message } );
40+
var result = options.CreateBadRequest( context.Request, code, message, null );
3741
await result.ExecuteResultAsync( actionContext );
3842
}
3943

4044
public static implicit operator RequestDelegate( BadRequestHandler handler ) => handler.ExecuteAsync;
4145
}
42-
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace Microsoft.AspNetCore.Mvc.Versioning
2+
{
3+
using AspNetCore.Mvc;
4+
using Http;
5+
using System;
6+
7+
/// <summary>
8+
/// Represents the function invoked to create a HTTP 400 (Bad Request) response related to API versioning.
9+
/// </summary>
10+
/// <param name="request">The current <see cref="HttpRequest">HTTP request</see>.</param>
11+
/// <param name="code">The associated error code.</param>
12+
/// <param name="message">The error message.</param>
13+
/// <param name="messageDetail">The detailed error message, if any.</param>
14+
/// <returns>A <see cref="BadRequestObjectResult">HTTP response</see> representing for status code 400 (Bad Request).</returns>
15+
[CLSCompliant( false )]
16+
public delegate BadRequestObjectResult CreateBadRequestDelegate( HttpRequest request, string code, string message, string messageDetail );
17+
}

0 commit comments

Comments
 (0)