Skip to content

Commit 023f710

Browse files
committed
Fixed: HTTP 415 error at operations endpoint when OpenAPI enabled
1 parent 3fafe50 commit 023f710

1 file changed

Lines changed: 41 additions & 9 deletions

File tree

src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiActionDescriptorCollectionProvider.cs

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,23 @@ internal sealed class JsonApiActionDescriptorCollectionProvider : IActionDescrip
3131
private const int FilterScope = 10;
3232
private static readonly Type ErrorDocumentType = typeof(ErrorResponseDocument);
3333

34+
private static readonly ConsumesMediaTypeCollection RegularMediaTypes = new([
35+
JsonApiMediaType.Default,
36+
OpenApiMediaTypes.OpenApi,
37+
#pragma warning disable CS0618 // Type or member is obsolete
38+
OpenApiMediaTypes.RelaxedOpenApi
39+
#pragma warning restore CS0618 // Type or member is obsolete
40+
]);
41+
42+
private static readonly ConsumesMediaTypeCollection OperationsMediaTypes = new([
43+
JsonApiMediaType.AtomicOperations,
44+
OpenApiMediaTypes.AtomicOperationsWithOpenApi,
45+
#pragma warning disable CS0618 // Type or member is obsolete
46+
JsonApiMediaType.RelaxedAtomicOperations,
47+
OpenApiMediaTypes.RelaxedAtomicOperationsWithRelaxedOpenApi
48+
#pragma warning restore CS0618 // Type or member is obsolete
49+
]);
50+
3451
private readonly IActionDescriptorCollectionProvider _defaultProvider;
3552
private readonly IControllerResourceMapping _controllerResourceMapping;
3653
private readonly JsonApiEndpointMetadataProvider _jsonApiEndpointMetadataProvider;
@@ -220,14 +237,14 @@ private ActionDescriptor[] SetEndpointMetadata(ActionDescriptor descriptor)
220237
{
221238
case AtomicOperationsRequestMetadata atomicOperationsRequestMetadata:
222239
{
223-
SetConsumes(descriptor, atomicOperationsRequestMetadata.DocumentType, JsonApiMediaType.AtomicOperations);
240+
SetConsumes(descriptor, atomicOperationsRequestMetadata.DocumentType, OperationsMediaTypes);
224241
UpdateRequestBodyParameterDescriptor(descriptor, atomicOperationsRequestMetadata.DocumentType, null);
225242

226243
break;
227244
}
228245
case PrimaryRequestMetadata primaryRequestMetadata:
229246
{
230-
SetConsumes(descriptor, primaryRequestMetadata.DocumentType, JsonApiMediaType.Default);
247+
SetConsumes(descriptor, primaryRequestMetadata.DocumentType, RegularMediaTypes);
231248
UpdateRequestBodyParameterDescriptor(descriptor, primaryRequestMetadata.DocumentType, null);
232249

233250
break;
@@ -243,7 +260,7 @@ private ActionDescriptor[] SetEndpointMetadata(ActionDescriptor descriptor)
243260

244261
RemovePathParameter(relationshipDescriptor.Parameters, "relationshipName");
245262
ExpandTemplate(relationshipDescriptor.AttributeRouteInfo!, relationship.PublicName);
246-
SetConsumes(descriptor, documentType, JsonApiMediaType.Default);
263+
SetConsumes(descriptor, documentType, RegularMediaTypes);
247264
UpdateRequestBodyParameterDescriptor(relationshipDescriptor, documentType, relationship.PublicName);
248265

249266
descriptorsByRelationship[relationship] = relationshipDescriptor;
@@ -302,22 +319,23 @@ private ActionDescriptor[] SetEndpointMetadata(ActionDescriptor descriptor)
302319
return isNonPrimaryEndpoint ? descriptorsByRelationship.Values.ToArray() : [descriptor];
303320
}
304321

305-
private static void SetConsumes(ActionDescriptor descriptor, Type requestType, JsonApiMediaType mediaType)
322+
private static void SetConsumes(ActionDescriptor descriptor, Type requestType, ConsumesMediaTypeCollection mediaTypes)
306323
{
307324
RemoveFiltersForRequestBody(descriptor);
308325

309-
// This value doesn't actually appear in the OpenAPI document, but is only used to invoke
310-
// JsonApiRequestFormatMetadataProvider.GetSupportedContentTypes(), which determines the actual request content type.
311-
string contentType = mediaType.ToString();
312-
313326
if (descriptor is ControllerActionDescriptor controllerActionDescriptor &&
314327
controllerActionDescriptor.MethodInfo.GetCustomAttributes<ConsumesAttribute>().Any())
315328
{
316329
// A custom JSON:API action method with [Consumes] on it. Hide the attribute from Swashbuckle, so it uses our data in API Explorer.
317330
controllerActionDescriptor.MethodInfo = new MethodInfoWrapper(controllerActionDescriptor.MethodInfo, [typeof(ConsumesAttribute)]);
318331
}
319332

320-
descriptor.FilterDescriptors.Add(new FilterDescriptor(new ConsumesAttribute(requestType, contentType), FilterScope));
333+
// These media types don't actually appear in the OpenAPI document, but are used to invoke
334+
// JsonApiRequestFormatMetadataProvider.GetSupportedContentTypes(), which determines the actual request content type.
335+
// We need to pass all possible media types, otherwise ASP.NET Core's content negotiation returns HTTP 415.
336+
337+
var consumesAttribute = new ConsumesAttribute(requestType, mediaTypes.ContentType, mediaTypes.OtherContentTypes);
338+
descriptor.FilterDescriptors.Add(new FilterDescriptor(consumesAttribute, FilterScope));
321339
}
322340

323341
private static void RemoveFiltersForRequestBody(ActionDescriptor descriptor)
@@ -474,4 +492,18 @@ private static void SetNonPrimaryResponseMetadata(ActionDescriptor descriptor,
474492

475493
descriptorsByRelationship[relationship] = relationshipDescriptor;
476494
}
495+
496+
private sealed class ConsumesMediaTypeCollection
497+
{
498+
public string ContentType { get; }
499+
public string[] OtherContentTypes { get; }
500+
501+
public ConsumesMediaTypeCollection(JsonApiMediaType[] mediaTypes)
502+
{
503+
ArgumentGuard.NotNullNorEmpty(mediaTypes);
504+
505+
ContentType = mediaTypes[0].ToString();
506+
OtherContentTypes = mediaTypes.Skip(1).Select(mediaType => mediaType.ToString()).ToArray();
507+
}
508+
}
477509
}

0 commit comments

Comments
 (0)