Skip to content

Commit 7b08441

Browse files
author
Bart Koelman
committed
Fixes in content negotation. Validations did not run when POSTing with an invalid content type. This change additionally allows extensions proposed at json-api/json-api#1437. Added test + fixes for running an endpoint that is not exposed through JsonApiDotNetCore (and we should not interfere)
1 parent 48c6696 commit 7b08441

29 files changed

+478
-151
lines changed

benchmarks/Serialization/JsonApiSerializerBenchmarks.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using BenchmarkDotNet.Attributes;
33
using JsonApiDotNetCore.Configuration;
4-
using JsonApiDotNetCore.Graph;
54
using JsonApiDotNetCore.Internal.Contracts;
65
using JsonApiDotNetCore.Managers;
76
using JsonApiDotNetCore.Query;

src/Examples/JsonApiDotNetCoreExample/Controllers/TestValuesController.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,25 @@ public IActionResult Get()
1111
var result = new[] { "value" };
1212
return Ok(result);
1313
}
14+
15+
[HttpPost]
16+
public IActionResult Post(string name)
17+
{
18+
var result = "Hello, " + name;
19+
return Ok(result);
20+
}
21+
22+
[HttpPatch]
23+
public IActionResult Patch(string name)
24+
{
25+
var result = "Hello, " + name;
26+
return Ok(result);
27+
}
28+
29+
[HttpDelete]
30+
public IActionResult Delete()
31+
{
32+
return Ok("Deleted");
33+
}
1434
}
1535
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Microsoft.AspNetCore.Http;
2+
3+
namespace JsonApiDotNetCore.Extensions
4+
{
5+
public static class HttpContextExtensions
6+
{
7+
public static bool IsJsonApiRequest(this HttpContext httpContext)
8+
{
9+
string value = httpContext.Items["IsJsonApiRequest"] as string;
10+
return value == bool.TrueString;
11+
}
12+
13+
internal static void SetJsonApiRequest(this HttpContext httpContext)
14+
{
15+
httpContext.Items["IsJsonApiRequest"] = bool.TrueString;
16+
}
17+
}
18+
}

src/JsonApiDotNetCore/Formatters/JsonApiInputFormatter.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using System;
22
using System.Threading.Tasks;
3-
using JsonApiDotNetCore.Internal;
3+
using JsonApiDotNetCore.Extensions;
44
using Microsoft.AspNetCore.Mvc.Formatters;
55
using Microsoft.Extensions.DependencyInjection;
66

@@ -13,9 +13,7 @@ public bool CanRead(InputFormatterContext context)
1313
if (context == null)
1414
throw new ArgumentNullException(nameof(context));
1515

16-
var contentTypeString = context.HttpContext.Request.ContentType;
17-
18-
return contentTypeString == HeaderConstants.ContentType;
16+
return context.HttpContext.IsJsonApiRequest();
1917
}
2018

2119
public async Task<InputFormatterResult> ReadAsync(InputFormatterContext context)

src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using System;
22
using System.Threading.Tasks;
3-
using JsonApiDotNetCore.Internal;
3+
using JsonApiDotNetCore.Extensions;
44
using Microsoft.AspNetCore.Mvc.Formatters;
55
using Microsoft.Extensions.DependencyInjection;
66

@@ -13,10 +13,9 @@ public bool CanWriteResult(OutputFormatterCanWriteContext context)
1313
if (context == null)
1414
throw new ArgumentNullException(nameof(context));
1515

16-
var contentTypeString = context.HttpContext.Request.ContentType;
17-
18-
return string.IsNullOrEmpty(contentTypeString) || contentTypeString == HeaderConstants.ContentType;
16+
return context.HttpContext.IsJsonApiRequest();
1917
}
18+
2019
public async Task WriteAsync(OutputFormatterWriteContext context)
2120
{
2221
var writer = context.HttpContext.RequestServices.GetService<IJsonApiWriter>();

src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Text;
66
using System.Threading.Tasks;
77
using JsonApiDotNetCore.Exceptions;
8-
using JsonApiDotNetCore.Internal;
98
using JsonApiDotNetCore.Middleware;
109
using JsonApiDotNetCore.Models.JsonApiDocuments;
1110
using JsonApiDotNetCore.Serialization.Server;
@@ -50,7 +49,7 @@ public async Task WriteAsync(OutputFormatterWriteContext context)
5049
}
5150
else
5251
{
53-
response.ContentType = HeaderConstants.ContentType;
52+
response.ContentType = HeaderConstants.MediaType;
5453
try
5554
{
5655
responseContent = SerializeResponse(context.Object, (HttpStatusCode)response.StatusCode);

src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
using JsonApiDotNetCore.Models;
1010
using Microsoft.AspNetCore.Mvc;
1111
using Microsoft.AspNetCore.Mvc.ApplicationModels;
12-
using Microsoft.Extensions.Options;
1312
using Newtonsoft.Json.Serialization;
1413

1514
namespace JsonApiDotNetCore.Internal

src/JsonApiDotNetCore/Internal/HeaderConstants.cs

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using JsonApiDotNetCore.Extensions;
12
using Microsoft.AspNetCore.Mvc;
23
using Microsoft.AspNetCore.Mvc.Filters;
34
using Microsoft.AspNetCore.Mvc.Infrastructure;
@@ -12,6 +13,11 @@ public void OnResultExecuted(ResultExecutedContext context)
1213

1314
public void OnResultExecuting(ResultExecutingContext context)
1415
{
16+
if (!context.HttpContext.IsJsonApiRequest())
17+
{
18+
return;
19+
}
20+
1521
if (context.Result is ObjectResult objectResult && objectResult.Value != null)
1622
{
1723
return;

src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs

Lines changed: 34 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
using System;
21
using System.IO;
32
using System.Linq;
43
using System.Net;
4+
using System.Net.Http.Headers;
55
using System.Threading.Tasks;
66
using JsonApiDotNetCore.Configuration;
77
using JsonApiDotNetCore.Extensions;
@@ -54,12 +54,17 @@ public async Task Invoke(HttpContext httpContext,
5454
_currentRequest.BasePath = GetBasePath(requestResource.ResourceName);
5555
_currentRequest.BaseId = GetBaseId();
5656
_currentRequest.RelationshipId = GetRelationshipId();
57-
}
5857

59-
if (await IsValidAsync())
60-
{
61-
await _next(httpContext);
58+
if (await IsValidAsync())
59+
{
60+
_httpContext.SetJsonApiRequest();
61+
await _next(httpContext);
62+
}
63+
64+
return;
6265
}
66+
67+
await _next(httpContext);
6368
}
6469

6570
private string GetBaseId()
@@ -139,60 +144,50 @@ private async Task<bool> IsValidAsync()
139144
private async Task<bool> IsValidContentTypeHeaderAsync(HttpContext context)
140145
{
141146
var contentType = context.Request.ContentType;
142-
if (contentType != null && ContainsMediaTypeParameters(contentType))
147+
if (contentType != null)
143148
{
144-
await FlushResponseAsync(context, new Error(HttpStatusCode.UnsupportedMediaType)
149+
if (!MediaTypeHeaderValue.TryParse(contentType, out var headerValue) ||
150+
headerValue.MediaType != HeaderConstants.MediaType || headerValue.CharSet != null ||
151+
headerValue.Parameters.Any(p => p.Name != "ext"))
145152
{
146-
Title = "The specified Content-Type header value is not supported.",
147-
Detail = $"Please specify '{HeaderConstants.ContentType}' for the Content-Type header value."
148-
});
153+
await FlushResponseAsync(context, new Error(HttpStatusCode.UnsupportedMediaType)
154+
{
155+
Title = "The specified Content-Type header value is not supported.",
156+
Detail = $"Please specify '{HeaderConstants.MediaType}' instead of '{contentType}' for the Content-Type header value."
157+
});
149158

150-
return false;
159+
return false;
160+
}
151161
}
162+
152163
return true;
153164
}
154165

155166
private async Task<bool> IsValidAcceptHeaderAsync(HttpContext context)
156167
{
157-
if (context.Request.Headers.TryGetValue(HeaderConstants.AcceptHeader, out StringValues acceptHeaders) == false)
158-
return true;
159-
160-
foreach (var acceptHeader in acceptHeaders)
168+
if (context.Request.Headers.TryGetValue("Accept", out StringValues acceptHeaders))
161169
{
162-
if (ContainsMediaTypeParameters(acceptHeader) == false)
170+
foreach (var acceptHeader in acceptHeaders)
163171
{
164-
continue;
172+
if (MediaTypeHeaderValue.TryParse(acceptHeader, out var headerValue))
173+
{
174+
if (headerValue.MediaType == HeaderConstants.MediaType &&
175+
headerValue.Parameters.All(p => p.Name == "ext"))
176+
{
177+
return true;
178+
}
179+
}
165180
}
166181

167182
await FlushResponseAsync(context, new Error(HttpStatusCode.NotAcceptable)
168183
{
169184
Title = "The specified Accept header value is not supported.",
170-
Detail = $"Please specify '{HeaderConstants.ContentType}' for the Accept header value."
185+
Detail = $"Please include '{HeaderConstants.MediaType}' in the Accept header values."
171186
});
172187
return false;
173188
}
174-
return true;
175-
}
176-
177-
private static bool ContainsMediaTypeParameters(string mediaType)
178-
{
179-
var incomingMediaTypeSpan = mediaType.AsSpan();
180189

181-
// if the content type is not application/vnd.api+json then continue on
182-
if (incomingMediaTypeSpan.Length < HeaderConstants.ContentType.Length)
183-
{
184-
return false;
185-
}
186-
187-
var incomingContentType = incomingMediaTypeSpan.Slice(0, HeaderConstants.ContentType.Length);
188-
if (incomingContentType.SequenceEqual(HeaderConstants.ContentType.AsSpan()) == false)
189-
return false;
190-
191-
// anything appended to "application/vnd.api+json;" will be considered a media type param
192-
return (
193-
incomingMediaTypeSpan.Length >= HeaderConstants.ContentType.Length + 2
194-
&& incomingMediaTypeSpan[HeaderConstants.ContentType.Length] == ';'
195-
);
190+
return true;
196191
}
197192

198193
private async Task FlushResponseAsync(HttpContext context, Error error)

0 commit comments

Comments
 (0)