From 023f71064edbda996c78b3aa863c4b030f51666d Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Sat, 11 Apr 2026 15:38:58 +0200 Subject: [PATCH] Fixed: HTTP 415 error at operations endpoint when OpenAPI enabled --- ...onApiActionDescriptorCollectionProvider.cs | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiActionDescriptorCollectionProvider.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiActionDescriptorCollectionProvider.cs index 77be1e7ce1..243072a72f 100644 --- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiActionDescriptorCollectionProvider.cs +++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiActionDescriptorCollectionProvider.cs @@ -31,6 +31,23 @@ internal sealed class JsonApiActionDescriptorCollectionProvider : IActionDescrip private const int FilterScope = 10; private static readonly Type ErrorDocumentType = typeof(ErrorResponseDocument); + private static readonly ConsumesMediaTypeCollection RegularMediaTypes = new([ + JsonApiMediaType.Default, + OpenApiMediaTypes.OpenApi, +#pragma warning disable CS0618 // Type or member is obsolete + OpenApiMediaTypes.RelaxedOpenApi +#pragma warning restore CS0618 // Type or member is obsolete + ]); + + private static readonly ConsumesMediaTypeCollection OperationsMediaTypes = new([ + JsonApiMediaType.AtomicOperations, + OpenApiMediaTypes.AtomicOperationsWithOpenApi, +#pragma warning disable CS0618 // Type or member is obsolete + JsonApiMediaType.RelaxedAtomicOperations, + OpenApiMediaTypes.RelaxedAtomicOperationsWithRelaxedOpenApi +#pragma warning restore CS0618 // Type or member is obsolete + ]); + private readonly IActionDescriptorCollectionProvider _defaultProvider; private readonly IControllerResourceMapping _controllerResourceMapping; private readonly JsonApiEndpointMetadataProvider _jsonApiEndpointMetadataProvider; @@ -220,14 +237,14 @@ private ActionDescriptor[] SetEndpointMetadata(ActionDescriptor descriptor) { case AtomicOperationsRequestMetadata atomicOperationsRequestMetadata: { - SetConsumes(descriptor, atomicOperationsRequestMetadata.DocumentType, JsonApiMediaType.AtomicOperations); + SetConsumes(descriptor, atomicOperationsRequestMetadata.DocumentType, OperationsMediaTypes); UpdateRequestBodyParameterDescriptor(descriptor, atomicOperationsRequestMetadata.DocumentType, null); break; } case PrimaryRequestMetadata primaryRequestMetadata: { - SetConsumes(descriptor, primaryRequestMetadata.DocumentType, JsonApiMediaType.Default); + SetConsumes(descriptor, primaryRequestMetadata.DocumentType, RegularMediaTypes); UpdateRequestBodyParameterDescriptor(descriptor, primaryRequestMetadata.DocumentType, null); break; @@ -243,7 +260,7 @@ private ActionDescriptor[] SetEndpointMetadata(ActionDescriptor descriptor) RemovePathParameter(relationshipDescriptor.Parameters, "relationshipName"); ExpandTemplate(relationshipDescriptor.AttributeRouteInfo!, relationship.PublicName); - SetConsumes(descriptor, documentType, JsonApiMediaType.Default); + SetConsumes(descriptor, documentType, RegularMediaTypes); UpdateRequestBodyParameterDescriptor(relationshipDescriptor, documentType, relationship.PublicName); descriptorsByRelationship[relationship] = relationshipDescriptor; @@ -302,14 +319,10 @@ private ActionDescriptor[] SetEndpointMetadata(ActionDescriptor descriptor) return isNonPrimaryEndpoint ? descriptorsByRelationship.Values.ToArray() : [descriptor]; } - private static void SetConsumes(ActionDescriptor descriptor, Type requestType, JsonApiMediaType mediaType) + private static void SetConsumes(ActionDescriptor descriptor, Type requestType, ConsumesMediaTypeCollection mediaTypes) { RemoveFiltersForRequestBody(descriptor); - // This value doesn't actually appear in the OpenAPI document, but is only used to invoke - // JsonApiRequestFormatMetadataProvider.GetSupportedContentTypes(), which determines the actual request content type. - string contentType = mediaType.ToString(); - if (descriptor is ControllerActionDescriptor controllerActionDescriptor && controllerActionDescriptor.MethodInfo.GetCustomAttributes().Any()) { @@ -317,7 +330,12 @@ private static void SetConsumes(ActionDescriptor descriptor, Type requestType, J controllerActionDescriptor.MethodInfo = new MethodInfoWrapper(controllerActionDescriptor.MethodInfo, [typeof(ConsumesAttribute)]); } - descriptor.FilterDescriptors.Add(new FilterDescriptor(new ConsumesAttribute(requestType, contentType), FilterScope)); + // These media types don't actually appear in the OpenAPI document, but are used to invoke + // JsonApiRequestFormatMetadataProvider.GetSupportedContentTypes(), which determines the actual request content type. + // We need to pass all possible media types, otherwise ASP.NET Core's content negotiation returns HTTP 415. + + var consumesAttribute = new ConsumesAttribute(requestType, mediaTypes.ContentType, mediaTypes.OtherContentTypes); + descriptor.FilterDescriptors.Add(new FilterDescriptor(consumesAttribute, FilterScope)); } private static void RemoveFiltersForRequestBody(ActionDescriptor descriptor) @@ -474,4 +492,18 @@ private static void SetNonPrimaryResponseMetadata(ActionDescriptor descriptor, descriptorsByRelationship[relationship] = relationshipDescriptor; } + + private sealed class ConsumesMediaTypeCollection + { + public string ContentType { get; } + public string[] OtherContentTypes { get; } + + public ConsumesMediaTypeCollection(JsonApiMediaType[] mediaTypes) + { + ArgumentGuard.NotNullNorEmpty(mediaTypes); + + ContentType = mediaTypes[0].ToString(); + OtherContentTypes = mediaTypes.Skip(1).Select(mediaType => mediaType.ToString()).ToArray(); + } + } }