Skip to content

Commit

Permalink
Updates 400 responses to use the standard error response format with …
Browse files Browse the repository at this point in the history
…support to change the format. Fixes #26 (#29)
  • Loading branch information
commonsensesoftware authored Sep 17, 2016
1 parent 6464a49 commit 239ec61
Show file tree
Hide file tree
Showing 10 changed files with 323 additions and 38 deletions.
2 changes: 1 addition & 1 deletion src/Common/Versioning/ApiVersioningOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Mvc.Versioning
/// <summary>
/// Represents the possible API versioning options for services.
/// </summary>
public class ApiVersioningOptions
public partial class ApiVersioningOptions
{
private ApiVersion defaultApiVersion = ApiVersion.Default;
private IApiVersionReader apiVersionReader = new QueryStringApiVersionReader();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
using System.Web.Http.Dispatcher;
using Versioning;
using static Controllers.HttpControllerDescriptorComparer;
using static System.Net.HttpStatusCode;
using static System.StringComparer;

/// <summary>
Expand Down Expand Up @@ -165,9 +164,9 @@ private static void EnsureRequestHasValidApiVersion( HttpRequestMessage request
}
catch ( AmbiguousApiVersionException ex )
{
var error = new HttpError( ex.Message ) { ["Code"] = "AmbiguousApiVersion" };
throw new HttpResponseException( request.CreateErrorResponse( BadRequest, error ) );
var options = request.GetApiVersioningOptions();
throw new HttpResponseException( options.CreateBadRequest( request, "AmbiguousApiVersion", ex.Message, null ) );
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Web.Http;
using System.Web.Http.Dispatcher;
using System.Web.Http.Tracing;
using Versioning;
using static ApiVersion;
using static System.Net.HttpStatusCode;

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

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

private ApiVersioningOptions Options => request.GetApiVersioningOptions();

[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created exception cannot be disposed. Handled by the caller." )]
internal HttpResponseException NewNotFoundOrBadRequestException( ControllerSelectionResult conventionRouteResult, ControllerSelectionResult directRouteResult ) =>
CreateBadRequestForUnsupportedApiVersion( conventionRouteResult, directRouteResult ) ?? CreateBadRequestForInvalidApiVersion() ?? CreateNotFound( conventionRouteResult );

[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created exception cannot be disposed. Handled by the caller." )]
internal HttpResponseMessage CreateBadRequestResponseForUnsupportedApiVersion( ApiVersion requestedVersion )
{
Contract.Requires( requestedVersion != null );
Contract.Ensures( Contract.Result<HttpResponseMessage>() != null );

var message = SR.VersionedResourceNotSupported.FormatDefault( request.RequestUri, requestedVersion );
var messageDetail = SR.VersionedControllerNameNotFound.FormatDefault( request.RequestUri, requestedVersion );

TraceWriter.Info( request, ControllerSelectorCategory, message );

return Options.CreateBadRequest( request, "UnsupportedApiVersion", message, messageDetail );
}

[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created exception cannot be disposed. Handled by the caller." )]
internal HttpResponseException CreateBadRequestForUnsupportedApiVersion( ApiVersion requestedVersion )
{
Contract.Requires( requestedVersion != null );
Contract.Ensures( Contract.Result<HttpResponseException>() != null );

return new HttpResponseException( CreateBadRequestResponseForUnsupportedApiVersion( requestedVersion ) );
}

[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created exception cannot be disposed. Handled by the caller." )]
private HttpResponseException CreateBadRequestForUnsupportedApiVersion( ControllerSelectionResult conventionRouteResult, ControllerSelectionResult directRouteResult )
{
Expand All @@ -47,14 +73,7 @@ private HttpResponseException CreateBadRequestForUnsupportedApiVersion( Controll
return null;
}

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

error["Code"] = "UnsupportedApiVersion";
TraceWriter.Info( request, ControllerSelectorCategory, message );

return new HttpResponseException( request.CreateErrorResponse( BadRequest, error ) );
return CreateBadRequestForUnsupportedApiVersion( requestedVersion );
}

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

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

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

return new HttpResponseException( request.CreateErrorResponse( BadRequest, error ) );
return new HttpResponseException( Options.CreateBadRequest( request, "InvalidApiVersion", message, messageDetail ) );
}

[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Created exception cannot be disposed. Handled by the caller." )]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
namespace Microsoft.Web.Http.Versioning
{
using System.Diagnostics.Contracts;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Web.Http;
using static System.Net.HttpStatusCode;
using static System.String;

/// <content>
/// Provides additional implementation specific to Microsoft ASP.NET Web API.
/// </content>
public partial class ApiVersioningOptions
{
private CreateBadRequestDelegate createBadRequest = CreateDefaultBadRequest;

/// <summary>
/// Gets or sets the function to used to create HTTP 400 (Bad Request) responses related to API versioning.
/// </summary>
/// <value>The <see cref="CreateBadRequestDelegate">function</see> to used to create a HTTP 400 (Bad Request)
/// <see cref="HttpResponseMessage">response</see> related to API versioning.</value>
/// <remarks>The default value generates responses that are compliant with the Microsoft REST API Guidelines.
/// This option should only be changed by service authors that intentionally want to deviate from the
/// established guidance.</remarks>
public CreateBadRequestDelegate CreateBadRequest
{
get
{
Contract.Ensures( createBadRequest != null );
return createBadRequest;
}
set
{
Arg.NotNull( value, nameof( value ) );
createBadRequest = value;
}
}

private static HttpResponseMessage CreateDefaultBadRequest( HttpRequestMessage request, string code, string message, string messageDetail )
{
if ( request == null || !IsODataRequest( request ) )
{
return CreateWebApiBadRequest( request, code, message, messageDetail );
}

return CreateODataBadRequest( request, code, message, messageDetail );
}

private static HttpResponseMessage CreateWebApiBadRequest( HttpRequestMessage request, string code, string message, string messageDetail )
{
var error = new HttpError();
var root = new HttpError() { ["Error"] = error };

if ( !IsNullOrEmpty( code ) )
{
error["Code"] = code;
}

if ( !IsNullOrEmpty( message ) )
{
error.Message = message;
}

if ( !IsNullOrEmpty( messageDetail ) && request?.ShouldIncludeErrorDetail() == true )
{
error["InnerError"] = new HttpError( messageDetail );
}

if ( request == null )
{
return new HttpResponseMessage( BadRequest )
{
Content = new ObjectContent<HttpError>( root, new JsonMediaTypeFormatter() )
};
}

return request.CreateErrorResponse( BadRequest, root );
}

private static bool IsODataRequest( HttpRequestMessage request )
{
if ( request == null )
{
return false;
}

var routeValues = request.GetRouteData();

if ( routeValues == null )
{
return false;
}

if ( !routeValues.Values.ContainsKey( "odataPath" ) )
{
return false;
}

return request.GetConfiguration()?.Formatters.JsonFormatter == null;
}

private static HttpResponseMessage CreateODataBadRequest( HttpRequestMessage request, string code, string message, string messageDetail )
{
Contract.Requires( request != null );

var error = new HttpError();

if ( !IsNullOrEmpty( code ) )
{
error[HttpErrorKeys.ErrorCodeKey] = code;
}

if ( !IsNullOrEmpty( message ) )
{
error.Message = message;
}

if ( !IsNullOrEmpty( messageDetail ) && request?.ShouldIncludeErrorDetail() == true )
{
error[HttpErrorKeys.MessageDetailKey] = messageDetail;
}

return request.CreateErrorResponse( BadRequest, error );
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Microsoft.Web.Http.Versioning
{
using System;
using System.Net.Http;
using System.Web.Http;

/// <summary>
/// Represents the function invoked to create a HTTP 400 (Bad Request) response related to API versioning.
/// </summary>
/// <param name="request">The current <see cref="HttpRequestMessage">HTTP request</see>.</param>
/// <param name="code">The associated error code.</param>
/// <param name="message">The error message.</param>
/// <param name="messageDetail">The detailed error message, if any.</param>
/// <returns>A <see cref="HttpResponseMessage">HTTP response</see> representing for status code 400 (Bad Request).</returns>
public delegate HttpResponseMessage CreateBadRequestDelegate( HttpRequestMessage request, string code, string message, string messageDetail );
}
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ private BadRequestHandler VerifyRequestedApiVersionIsNotAmbiguous( HttpContext h
{
logger.LogInformation( ex.Message );
apiVersion = default( ApiVersion );
return new BadRequestHandler( "AmbiguousApiVersion", ex.Message );
return new BadRequestHandler( Options, "AmbiguousApiVersion", ex.Message );
}

return null;
Expand Down Expand Up @@ -251,7 +251,7 @@ private BadRequestHandler IsValidRequest( ActionSelectionContext context )
}

var message = SR.VersionedResourceNotSupported.FormatDefault( context.HttpContext.Request.GetDisplayUrl(), requestedVersion );
return new BadRequestHandler( code, message );
return new BadRequestHandler( Options, code, message );
}

private static IEnumerable<ActionDescriptor> MatchVersionNeutralActions( ActionSelectionContext context ) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
namespace Microsoft.AspNetCore.Mvc.Versioning
{
using Hosting;
using Http;
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using static System.String;

/// <content>
/// Provides additional implementation specific to Microsoft ASP.NET Web API.
/// </content>
public partial class ApiVersioningOptions
{
private CreateBadRequestDelegate createBadRequest = CreateDefaultBadRequest;

/// <summary>
/// Gets or sets the function to used to create HTTP 400 (Bad Request) responses related to API versioning.
/// </summary>
/// <value>The <see cref="CreateBadRequestDelegate">function</see> to used to create a HTTP 400 (Bad Request)
/// <see cref="HttpResponse">response</see> related to API versioning.</value>
/// <remarks>The default value generates responses that are compliant with the Microsoft REST API Guidelines.
/// This option should only be changed by service authors that intentionally want to deviate from the
/// established guidance.</remarks>
[CLSCompliant( false )]
public CreateBadRequestDelegate CreateBadRequest
{
get
{
Contract.Ensures( createBadRequest != null );
return createBadRequest;
}
set
{
Arg.NotNull( value, nameof( value ) );
createBadRequest = value;
}
}

private static BadRequestObjectResult CreateDefaultBadRequest( HttpRequest request, string code, string message, string messageDetail )
{
var error = new Dictionary<string, object>();
var root = new Dictionary<string, object>() { ["Error"] = error };

if ( !IsNullOrEmpty( code ) )
{
error["Code"] = code;
}

if ( !IsNullOrEmpty( message ) )
{
error["Message"] = message;
}

if ( !IsNullOrEmpty( messageDetail ) )
{
var environment = (IHostingEnvironment) request?.HttpContext.RequestServices.GetService( typeof( IHostingEnvironment ) );

if ( environment?.IsDevelopment() == true )
{
error["InnerError"] = new Dictionary<string, object>() { ["Message"] = messageDetail };
}
}

return new BadRequestObjectResult( root );
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@

internal sealed class BadRequestHandler
{
private readonly ApiVersioningOptions options;
private readonly string code;
private readonly string message;

internal BadRequestHandler( string message )
: this( null, message )
internal BadRequestHandler( ApiVersioningOptions options, string message )
: this( options, null, message )
{
}

internal BadRequestHandler( string code, string message )
internal BadRequestHandler( ApiVersioningOptions options, string code, string message )
{
Contract.Requires( options != null );
Contract.Requires( !string.IsNullOrEmpty( message ) );

this.options = options;
this.message = message;
this.code = code;
}
Expand All @@ -33,10 +37,10 @@ internal async Task ExecuteAsync( HttpContext context )
RouteData = context.GetRouteData(),
ActionDescriptor = new ActionDescriptor()
};
var result = new BadRequestObjectResult( new { Code = code, Message = message } );
var result = options.CreateBadRequest( context.Request, code, message, null );
await result.ExecuteResultAsync( actionContext );
}

public static implicit operator RequestDelegate( BadRequestHandler handler ) => handler.ExecuteAsync;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Microsoft.AspNetCore.Mvc.Versioning
{
using AspNetCore.Mvc;
using Http;
using System;

/// <summary>
/// Represents the function invoked to create a HTTP 400 (Bad Request) response related to API versioning.
/// </summary>
/// <param name="request">The current <see cref="HttpRequest">HTTP request</see>.</param>
/// <param name="code">The associated error code.</param>
/// <param name="message">The error message.</param>
/// <param name="messageDetail">The detailed error message, if any.</param>
/// <returns>A <see cref="BadRequestObjectResult">HTTP response</see> representing for status code 400 (Bad Request).</returns>
[CLSCompliant( false )]
public delegate BadRequestObjectResult CreateBadRequestDelegate( HttpRequest request, string code, string message, string messageDetail );
}
Loading

0 comments on commit 239ec61

Please sign in to comment.