From ee7d069a8caa142771af027a2ee6b0bc5f4092bf Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Fri, 10 Aug 2018 21:45:15 -0700 Subject: [PATCH] fix(#313): Do not return 409 for generic InvalidCastException --- .../IServiceCollectionExtensions.cs | 24 ++++----- .../Internal/JsonApiExceptionFactory.cs | 8 --- .../Middleware/TypeMatchFilter.cs | 49 +++++++++++++++++++ 3 files changed, 61 insertions(+), 20 deletions(-) create mode 100644 src/JsonApiDotNetCore/Middleware/TypeMatchFilter.cs diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 0199096316..4a56620b78 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -12,6 +12,7 @@ using JsonApiDotNetCore.Services.Operations.Processors; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -44,12 +45,7 @@ public static IServiceCollection AddJsonApi(this IServiceCollection se config.BuildContextGraph(builder => builder.AddDbContext()); - mvcBuilder - .AddMvcOptions(opt => - { - opt.Filters.Add(typeof(JsonApiExceptionFilter)); - opt.SerializeAsJsonApi(config); - }); + mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, config)); AddJsonApiInternals(services, config); return services; @@ -63,17 +59,19 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services, options(config); - mvcBuilder - .AddMvcOptions(opt => - { - opt.Filters.Add(typeof(JsonApiExceptionFilter)); - opt.SerializeAsJsonApi(config); - }); + mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, config)); AddJsonApiInternals(services, config); return services; } + private static void AddMvcOptions(MvcOptions options, JsonApiOptions config) + { + options.Filters.Add(typeof(JsonApiExceptionFilter)); + options.Filters.Add(typeof(TypeMatchFilter)); + options.SerializeAsJsonApi(config); + } + public static void AddJsonApiInternals( this IServiceCollection services, JsonApiOptions jsonApiOptions) where TContext : DbContext @@ -141,6 +139,8 @@ public static void AddJsonApiInternals( services.AddScoped(); services.AddScoped(); services.AddScoped(); + + // services.AddScoped(); } private static void AddOperationServices(IServiceCollection services) diff --git a/src/JsonApiDotNetCore/Internal/JsonApiExceptionFactory.cs b/src/JsonApiDotNetCore/Internal/JsonApiExceptionFactory.cs index 159c9abc70..3b95e85b01 100644 --- a/src/JsonApiDotNetCore/Internal/JsonApiExceptionFactory.cs +++ b/src/JsonApiDotNetCore/Internal/JsonApiExceptionFactory.cs @@ -11,14 +11,6 @@ public static JsonApiException GetException(Exception exception) if (exceptionType == typeof(JsonApiException)) return (JsonApiException)exception; - // TODO: this is for mismatching type requests (e.g. posting an author to articles endpoint) - // however, we can't actually guarantee that this is the source of this exception - // we should probably use an action filter or when we improve the ContextGraph - // we might be able to skip most of deserialization entirely by checking the JToken - // directly - if (exceptionType == typeof(InvalidCastException)) - return new JsonApiException(409, exception.Message, exception); - return new JsonApiException(500, exceptionType.Name, exception); } } diff --git a/src/JsonApiDotNetCore/Middleware/TypeMatchFilter.cs b/src/JsonApiDotNetCore/Middleware/TypeMatchFilter.cs new file mode 100644 index 0000000000..5c061babe6 --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/TypeMatchFilter.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace JsonApiDotNetCore.Middleware +{ + public class TypeMatchFilter : IActionFilter + { + private readonly IJsonApiContext _jsonApiContext; + + public TypeMatchFilter(IJsonApiContext jsonApiContext) + { + _jsonApiContext = jsonApiContext; + } + + /// + /// Used to verify the incoming type matches the target type, else return a 409 + /// + public void OnActionExecuting(ActionExecutingContext context) + { + var request = context.HttpContext.Request; + if (IsJsonApiRequest(request) && request.Method == "PATCH" || request.Method == "POST") + { + var deserializedType = context.ActionArguments.FirstOrDefault().Value?.GetType(); + var targetType = context.ActionDescriptor.Parameters.FirstOrDefault()?.ParameterType; + + if (deserializedType != null && targetType != null && deserializedType != targetType) + { + var expectedJsonApiResource = _jsonApiContext.ContextGraph.GetContextEntity(targetType); + + throw new JsonApiException(409, + $"Cannot '{context.HttpContext.Request.Method}' type '{_jsonApiContext.RequestEntity.EntityName}' " + + $"to '{expectedJsonApiResource?.EntityName}' endpoint.", + detail: "Check that the request payload type matches the type expected by this endpoint."); + } + } + } + + private bool IsJsonApiRequest(HttpRequest request) + { + return (request.ContentType?.Equals(Constants.ContentType, StringComparison.OrdinalIgnoreCase) == true); + } + + public void OnActionExecuted(ActionExecutedContext context) { /* noop */ } + } +}