Skip to content

Commit 8ff14c5

Browse files
author
Bart Koelman
committed
Fixed: Allow partially matching accept headers as defined in HTTP standard (and block parameters)
1 parent 9d76d05 commit 8ff14c5

File tree

2 files changed

+86
-71
lines changed

2 files changed

+86
-71
lines changed

src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Generic;
12
using System.IO;
23
using System.Linq;
34
using System.Net;
@@ -77,44 +78,52 @@ private ResourceContext CreateResourceContext(IControllerResourceMapping control
7778
private async Task<bool> ValidateContentTypeHeaderAsync()
7879
{
7980
var contentType = _httpContext.Request.ContentType;
80-
if (contentType != null)
81+
if (contentType != null && contentType != HeaderConstants.MediaType)
8182
{
82-
if (!MediaTypeHeaderValue.TryParse(contentType, out var headerValue) ||
83-
headerValue.MediaType != HeaderConstants.MediaType || headerValue.CharSet != null ||
84-
headerValue.Parameters.Any(p => p.Name != "ext"))
83+
await FlushResponseAsync(_httpContext, new Error(HttpStatusCode.UnsupportedMediaType)
8584
{
86-
await FlushResponseAsync(_httpContext, new Error(HttpStatusCode.UnsupportedMediaType)
87-
{
88-
Title = "The specified Content-Type header value is not supported.",
89-
Detail = $"Please specify '{HeaderConstants.MediaType}' instead of '{contentType}' for the Content-Type header value."
90-
});
91-
92-
return false;
93-
}
85+
Title = "The specified Content-Type header value is not supported.",
86+
Detail = $"Please specify '{HeaderConstants.MediaType}' instead of '{contentType}' for the Content-Type header value."
87+
});
88+
return false;
9489
}
9590

9691
return true;
9792
}
9893

9994
private async Task<bool> ValidateAcceptHeaderAsync()
10095
{
101-
if (_httpContext.Request.Headers.TryGetValue("Accept", out StringValues acceptHeaders))
96+
StringValues acceptHeaders = _httpContext.Request.Headers["Accept"];
97+
if (!acceptHeaders.Any() || acceptHeaders == HeaderConstants.MediaType)
98+
{
99+
return true;
100+
}
101+
102+
bool seenCompatibleMediaType = false;
103+
104+
foreach (var acceptHeader in acceptHeaders)
102105
{
103-
foreach (var acceptHeader in acceptHeaders)
106+
if (MediaTypeHeaderValue.TryParse(acceptHeader, out var headerValue))
104107
{
105-
if (MediaTypeHeaderValue.TryParse(acceptHeader, out var headerValue))
108+
if (headerValue.MediaType == "*/*" || headerValue.MediaType == "application/*")
106109
{
107-
if (headerValue.MediaType == HeaderConstants.MediaType &&
108-
headerValue.Parameters.All(p => p.Name == "ext"))
109-
{
110-
return true;
111-
}
110+
seenCompatibleMediaType = true;
111+
break;
112+
}
113+
114+
if (headerValue.MediaType == HeaderConstants.MediaType && !headerValue.Parameters.Any())
115+
{
116+
seenCompatibleMediaType = true;
117+
break;
112118
}
113119
}
120+
}
114121

122+
if (!seenCompatibleMediaType)
123+
{
115124
await FlushResponseAsync(_httpContext, new Error(HttpStatusCode.NotAcceptable)
116125
{
117-
Title = "The specified Accept header value is not supported.",
126+
Title = "The specified Accept header value does not contain any supported media types.",
118127
Detail = $"Please include '{HeaderConstants.MediaType}' in the Accept header values."
119128
});
120129
return false;

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ContentNegotiationTests.cs

Lines changed: 56 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public async Task Respond_415_If_Content_Type_Header_Is_Not_JsonApi_Media_Type()
5050
var server = new TestServer(builder);
5151
var client = server.CreateClient();
5252
var request = new HttpRequestMessage(new HttpMethod("POST"), route) {Content = new StringContent(string.Empty)};
53-
request.Content.Headers.ContentType = new MediaTypeHeaderValue("text/html");
53+
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("text/html");
5454

5555
// Act
5656
var response = await client.SendAsync(request);
@@ -78,7 +78,7 @@ public async Task Respond_201_If_Content_Type_Header_Is_JsonApi_Media_Type()
7878
var server = new TestServer(builder);
7979
var client = server.CreateClient();
8080
var request = new HttpRequestMessage(HttpMethod.Post, route) {Content = new StringContent(serializer.Serialize(todoItem))};
81-
request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType);
81+
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(HeaderConstants.MediaType);
8282

8383
// Act
8484
var response = await client.SendAsync(request);
@@ -88,43 +88,65 @@ public async Task Respond_201_If_Content_Type_Header_Is_JsonApi_Media_Type()
8888
}
8989

9090
[Fact]
91-
public async Task Respond_201_If_Content_Type_Header_Is_JsonApi_Media_Type_With_Extension()
91+
public async Task Respond_415_If_Content_Type_Header_Is_JsonApi_Media_Type_With_Profile()
9292
{
9393
// Arrange
94-
var serializer = _fixture.GetSerializer<TodoItem>(e => new { e.Description });
95-
var todoItem = new TodoItem {Description = "something not to forget"};
94+
var builder = new WebHostBuilder().UseStartup<Startup>();
95+
var route = "/api/v1/todoItems";
96+
var server = new TestServer(builder);
97+
var client = server.CreateClient();
98+
var request = new HttpRequestMessage(new HttpMethod("POST"), route) {Content = new StringContent(string.Empty)};
99+
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(HeaderConstants.MediaType + "; profile=something");
100+
101+
// Act
102+
var response = await client.SendAsync(request);
96103

104+
// Assert
105+
Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode);
106+
107+
var body = await response.Content.ReadAsStringAsync();
108+
var errorDocument = JsonConvert.DeserializeObject<ErrorDocument>(body);
109+
Assert.Single(errorDocument.Errors);
110+
Assert.Equal(HttpStatusCode.UnsupportedMediaType, errorDocument.Errors[0].StatusCode);
111+
Assert.Equal("The specified Content-Type header value is not supported.", errorDocument.Errors[0].Title);
112+
Assert.Equal("Please specify 'application/vnd.api+json' instead of 'application/vnd.api+json; profile=something' for the Content-Type header value.", errorDocument.Errors[0].Detail);
113+
}
114+
115+
[Fact]
116+
public async Task Respond_415_If_Content_Type_Header_Is_JsonApi_Media_Type_With_Extension()
117+
{
97118
// Arrange
98119
var builder = new WebHostBuilder().UseStartup<Startup>();
99120
var route = "/api/v1/todoItems";
100121
var server = new TestServer(builder);
101122
var client = server.CreateClient();
102-
var request = new HttpRequestMessage(HttpMethod.Post, route) {Content = new StringContent(serializer.Serialize(todoItem))};
103-
request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType)
104-
{
105-
Parameters = {new NameValueHeaderValue("ext", "some-extension")}
106-
};
123+
var request = new HttpRequestMessage(new HttpMethod("POST"), route) {Content = new StringContent(string.Empty)};
124+
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(HeaderConstants.MediaType + "; ext=something");
107125

108126
// Act
109127
var response = await client.SendAsync(request);
110128

111129
// Assert
112-
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
130+
Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode);
131+
132+
var body = await response.Content.ReadAsStringAsync();
133+
var errorDocument = JsonConvert.DeserializeObject<ErrorDocument>(body);
134+
Assert.Single(errorDocument.Errors);
135+
Assert.Equal(HttpStatusCode.UnsupportedMediaType, errorDocument.Errors[0].StatusCode);
136+
Assert.Equal("The specified Content-Type header value is not supported.", errorDocument.Errors[0].Title);
137+
Assert.Equal("Please specify 'application/vnd.api+json' instead of 'application/vnd.api+json; ext=something' for the Content-Type header value.", errorDocument.Errors[0].Detail);
113138
}
114139

115140
[Fact]
116-
public async Task Respond_415_If_Content_Type_Header_Is_JsonApi_Media_Type_With_CharSet_Parameter()
141+
public async Task Respond_415_If_Content_Type_Header_Is_JsonApi_Media_Type_With_CharSet()
117142
{
118143
// Arrange
119144
var builder = new WebHostBuilder().UseStartup<Startup>();
120145
var route = "/api/v1/todoItems";
121146
var server = new TestServer(builder);
122147
var client = server.CreateClient();
123148
var request = new HttpRequestMessage(HttpMethod.Post, route) {Content = new StringContent(string.Empty)};
124-
request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType)
125-
{
126-
CharSet = "ISO-8859-4"
127-
};
149+
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(HeaderConstants.MediaType + "; charset=ISO-8859-4");
128150

129151
// Act
130152
var response = await client.SendAsync(request);
@@ -141,18 +163,15 @@ public async Task Respond_415_If_Content_Type_Header_Is_JsonApi_Media_Type_With_
141163
}
142164

143165
[Fact]
144-
public async Task Respond_415_If_Content_Type_Header_Is_JsonApi_Media_Type_With_Unknown_Parameter()
166+
public async Task Respond_415_If_Content_Type_Header_Is_JsonApi_Media_Type_With_Unknown()
145167
{
146168
// Arrange
147169
var builder = new WebHostBuilder().UseStartup<Startup>();
148170
var route = "/api/v1/todoItems";
149171
var server = new TestServer(builder);
150172
var client = server.CreateClient();
151173
var request = new HttpRequestMessage(HttpMethod.Post, route) {Content = new StringContent(string.Empty)};
152-
request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType)
153-
{
154-
Parameters = {new NameValueHeaderValue("unknown", "unexpected")}
155-
};
174+
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(HeaderConstants.MediaType + "; unknown=unexpected");
156175

157176
// Act
158177
var response = await client.SendAsync(request);
@@ -186,17 +205,15 @@ public async Task Respond_200_If_Accept_Headers_Are_Missing()
186205
}
187206

188207
[Fact]
189-
public async Task Respond_200_If_Accept_Headers_Contain_JsonApi_Media_Type()
208+
public async Task Respond_200_If_Accept_Headers_Include_Any()
190209
{
191210
// Arrange
192211
var builder = new WebHostBuilder().UseStartup<Startup>();
193212
var route = "/api/v1/todoItems";
194213
var server = new TestServer(builder);
195214
var client = server.CreateClient();
196215
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("text/html"));
197-
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; some=1"));
198-
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; other=2"));
199-
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType));
216+
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("*/*"));
200217
var request = new HttpRequestMessage(HttpMethod.Get, route);
201218

202219
// Act
@@ -207,23 +224,15 @@ public async Task Respond_200_If_Accept_Headers_Contain_JsonApi_Media_Type()
207224
}
208225

209226
[Fact]
210-
public async Task Respond_200_If_Accept_Headers_Contain_JsonApi_Media_Type_With_Extension()
227+
public async Task Respond_200_If_Accept_Headers_Include_Application_Prefix()
211228
{
212229
// Arrange
213230
var builder = new WebHostBuilder().UseStartup<Startup>();
214231
var route = "/api/v1/todoItems";
215232
var server = new TestServer(builder);
216233
var client = server.CreateClient();
217234
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("text/html"));
218-
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; some=1"));
219-
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; other=2"));
220-
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(HeaderConstants.MediaType)
221-
{
222-
Parameters =
223-
{
224-
new NameValueHeaderValue("ext", "some-extension")
225-
}
226-
});
235+
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/*"));
227236
var request = new HttpRequestMessage(HttpMethod.Get, route);
228237

229238
// Act
@@ -234,42 +243,39 @@ public async Task Respond_200_If_Accept_Headers_Contain_JsonApi_Media_Type_With_
234243
}
235244

236245
[Fact]
237-
public async Task Respond_406_If_Accept_Headers_Do_Not_Contain_JsonApi_Media_Type()
246+
public async Task Respond_200_If_Accept_Headers_Contain_JsonApi_Media_Type()
238247
{
239248
// Arrange
240249
var builder = new WebHostBuilder().UseStartup<Startup>();
241250
var route = "/api/v1/todoItems";
242251
var server = new TestServer(builder);
243252
var client = server.CreateClient();
244253
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("text/html"));
245-
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("image/*"));
254+
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; profile=some"));
255+
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; ext=other"));
256+
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; unknown=unexpected"));
257+
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType));
246258
var request = new HttpRequestMessage(HttpMethod.Get, route);
247259

248260
// Act
249261
var response = await client.SendAsync(request);
250262

251263
// Assert
252-
Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode);
253-
254-
var body = await response.Content.ReadAsStringAsync();
255-
var errorDocument = JsonConvert.DeserializeObject<ErrorDocument>(body);
256-
Assert.Single(errorDocument.Errors);
257-
Assert.Equal(HttpStatusCode.NotAcceptable, errorDocument.Errors[0].StatusCode);
258-
Assert.Equal("The specified Accept header value is not supported.", errorDocument.Errors[0].Title);
259-
Assert.Equal("Please include 'application/vnd.api+json' in the Accept header values.", errorDocument.Errors[0].Detail);
264+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
260265
}
261266

262267
[Fact]
263-
public async Task Respond_406_If_Accept_Headers_Contain_JsonApi_Media_Type_With_Only_Parameters_Other_Than_Extension()
268+
public async Task Respond_406_If_Accept_Headers_Only_Contain_JsonApi_Media_Type_With_Parameters()
264269
{
265270
// Arrange
266271
var builder = new WebHostBuilder().UseStartup<Startup>();
267272
var route = "/api/v1/todoItems";
268273
var server = new TestServer(builder);
269274
var client = server.CreateClient();
270-
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("text/html"));
271-
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; some=1"));
272-
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; other=2"));
275+
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; profile=some"));
276+
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; ext=other"));
277+
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; unknown=unexpected"));
278+
client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; charset=ISO-8859-4"));
273279
var request = new HttpRequestMessage(HttpMethod.Get, route);
274280

275281
// Act
@@ -282,7 +288,7 @@ public async Task Respond_406_If_Accept_Headers_Contain_JsonApi_Media_Type_With_
282288
var errorDocument = JsonConvert.DeserializeObject<ErrorDocument>(body);
283289
Assert.Single(errorDocument.Errors);
284290
Assert.Equal(HttpStatusCode.NotAcceptable, errorDocument.Errors[0].StatusCode);
285-
Assert.Equal("The specified Accept header value is not supported.", errorDocument.Errors[0].Title);
291+
Assert.Equal("The specified Accept header value does not contain any supported media types.", errorDocument.Errors[0].Title);
286292
Assert.Equal("Please include 'application/vnd.api+json' in the Accept header values.", errorDocument.Errors[0].Detail);
287293
}
288294
}

0 commit comments

Comments
 (0)