Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

Commit 54d1b10

Browse files
committed
[Fixes #4945] Simple string returned by controller action is not a valid JSON!
1 parent 305748a commit 54d1b10

File tree

10 files changed

+335
-192
lines changed

10 files changed

+335
-192
lines changed

src/Microsoft.AspNetCore.Mvc.Core/Formatters/StringOutputFormatter.cs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
namespace Microsoft.AspNetCore.Mvc.Formatters
1212
{
1313
/// <summary>
14-
/// Always writes a string value to the response, regardless of requested content type.
14+
/// A <see cref="TextOutputFormatter"/> for simple text content.
1515
/// </summary>
1616
public class StringOutputFormatter : TextOutputFormatter
1717
{
@@ -29,18 +29,10 @@ public override bool CanWriteResult(OutputFormatterCanWriteContext context)
2929
throw new ArgumentNullException(nameof(context));
3030
}
3131

32-
// Ignore the passed in content type, if the object is string
33-
// always return it as a text/plain format.
3432
if (context.ObjectType == typeof(string) || context.Object is string)
3533
{
36-
if (!context.ContentType.HasValue)
37-
{
38-
var mediaType = SupportedMediaTypes[0];
39-
var encoding = SupportedEncodings[0];
40-
context.ContentType = new StringSegment(MediaType.ReplaceEncoding(mediaType, encoding));
41-
}
42-
43-
return true;
34+
// Call into base to check if the current request's content type is a supported media type.
35+
return base.CanWriteResult(context);
4436
}
4537

4638
return false;

test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/StringOutputFormatterTests.cs

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,39 +12,52 @@
1212

1313
namespace Microsoft.AspNetCore.Mvc.Formatters
1414
{
15-
public class TextPlainFormatterTests
15+
public class StringOutputFormatterTests
1616
{
17-
public static IEnumerable<object[]> OutputFormatterContextValues
17+
public static IEnumerable<object[]> CanWriteStringsData
1818
{
1919
get
2020
{
21-
// object value, bool useDeclaredTypeAsString, bool expectedCanWriteResult
22-
yield return new object[] { "valid value", true, true };
23-
yield return new object[] { null, true, true };
24-
yield return new object[] { null, false, false };
25-
yield return new object[] { new object(), false, false };
21+
// object value, bool useDeclaredTypeAsString
22+
yield return new object[] { "declared and runtime type are same", true };
23+
yield return new object[] { "declared and runtime type are different", false };
24+
yield return new object[] { null, true };
2625
}
2726
}
2827

29-
[Fact]
30-
public void CanWriteResult_SetsAcceptContentType()
28+
public static TheoryData<object> CannotWriteNonStringsData
29+
{
30+
get
31+
{
32+
return new TheoryData<object>()
33+
{
34+
null,
35+
new object()
36+
};
37+
}
38+
}
39+
40+
[Theory]
41+
[InlineData("application/json")]
42+
[InlineData("application/xml")]
43+
public void CannotWriteUnsupportedMediaType(string contentType)
3144
{
3245
// Arrange
3346
var formatter = new StringOutputFormatter();
34-
var expectedContentType = new StringSegment("application/json");
47+
var expectedContentType = new StringSegment(contentType);
3548

3649
var context = new OutputFormatterWriteContext(
3750
new DefaultHttpContext(),
3851
new TestHttpResponseStreamWriterFactory().CreateWriter,
3952
typeof(string),
4053
"Thisisastring");
41-
context.ContentType = expectedContentType;
54+
context.ContentType = new StringSegment(contentType);
4255

4356
// Act
4457
var result = formatter.CanWriteResult(context);
4558

4659
// Assert
47-
Assert.True(result);
60+
Assert.False(result);
4861
Assert.Equal(expectedContentType, context.ContentType);
4962
}
5063

@@ -53,7 +66,6 @@ public void CanWriteResult_DefaultContentType()
5366
{
5467
// Arrange
5568
var formatter = new StringOutputFormatter();
56-
5769
var context = new OutputFormatterWriteContext(
5870
new DefaultHttpContext(),
5971
new TestHttpResponseStreamWriterFactory().CreateWriter,
@@ -65,18 +77,17 @@ public void CanWriteResult_DefaultContentType()
6577

6678
// Assert
6779
Assert.True(result);
68-
Assert.Equal(new StringSegment("text/plain; charset=utf-8"), context.ContentType);
80+
Assert.Equal(new StringSegment("text/plain"), context.ContentType);
6981
}
7082

7183
[Theory]
72-
[MemberData(nameof(OutputFormatterContextValues))]
73-
public void CanWriteResult_ReturnsTrueForStringTypes(
84+
[MemberData(nameof(CanWriteStringsData))]
85+
public void CanWriteStrings(
7486
object value,
75-
bool useDeclaredTypeAsString,
76-
bool expectedCanWriteResult)
87+
bool useDeclaredTypeAsString)
7788
{
7889
// Arrange
79-
var expectedContentType = new StringSegment("application/json");
90+
var expectedContentType = new StringSegment("text/plain");
8091

8192
var formatter = new StringOutputFormatter();
8293
var type = useDeclaredTypeAsString ? typeof(string) : typeof(object);
@@ -86,13 +97,35 @@ public void CanWriteResult_ReturnsTrueForStringTypes(
8697
new TestHttpResponseStreamWriterFactory().CreateWriter,
8798
type,
8899
value);
89-
context.ContentType = expectedContentType;
100+
context.ContentType = new StringSegment("text/plain");
101+
102+
// Act
103+
var result = formatter.CanWriteResult(context);
104+
105+
// Assert
106+
Assert.True(result);
107+
Assert.Equal(expectedContentType, context.ContentType);
108+
}
109+
110+
[Theory]
111+
[MemberData(nameof(CannotWriteNonStringsData))]
112+
public void CannotWriteNonStrings(object value)
113+
{
114+
// Arrange
115+
var expectedContentType = new StringSegment("text/plain");
116+
var formatter = new StringOutputFormatter();
117+
var context = new OutputFormatterWriteContext(
118+
new DefaultHttpContext(),
119+
new TestHttpResponseStreamWriterFactory().CreateWriter,
120+
typeof(object),
121+
value);
122+
context.ContentType = new StringSegment("text/plain");
90123

91124
// Act
92125
var result = formatter.CanWriteResult(context);
93126

94127
// Assert
95-
Assert.Equal(expectedCanWriteResult, result);
128+
Assert.False(result);
96129
Assert.Equal(expectedContentType, context.ContentType);
97130
}
98131

test/Microsoft.AspNetCore.Mvc.FunctionalTests/ContentNegotiationTest.cs

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -345,33 +345,31 @@ public async Task XmlFormatter_SupportedMediaType_DoesNotChangeAcrossRequests()
345345
}
346346

347347
[Theory]
348-
[InlineData(true)]
349-
[InlineData(false)]
350-
public async Task ObjectResult_WithStringReturnType_DefaultToTextPlain(bool matchFormatterOnObjectType)
348+
[InlineData(null)]
349+
[InlineData("text/plain")]
350+
[InlineData("text/plain; charset=utf-8")]
351+
[InlineData("text/html, application/xhtml+xml, image/jxr, */*")] // typical browser accept header
352+
public async Task ObjectResult_WithStringReturnType_DefaultToTextPlain(string acceptMediaType)
351353
{
352354
// Arrange
353-
var targetUri = "http://localhost/FallbackOnTypeBasedMatch/ReturnString?matchFormatterOnObjectType=true" +
354-
matchFormatterOnObjectType;
355-
var request = new HttpRequestMessage(HttpMethod.Get, targetUri);
355+
var request = new HttpRequestMessage(HttpMethod.Get, "FallbackOnTypeBasedMatch/ReturnString");
356+
request.Headers.Accept.ParseAdd(acceptMediaType);
356357

357358
// Act
358359
var response = await Client.SendAsync(request);
359360

360361
// Assert
361362
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
362-
Assert.Equal("text/plain", response.Content.Headers.ContentType.MediaType);
363+
Assert.Equal("text/plain; charset=utf-8", response.Content.Headers.ContentType.ToString());
363364
var actualBody = await response.Content.ReadAsStringAsync();
364365
Assert.Equal("Hello World!", actualBody);
365366
}
366367

367-
[Theory]
368-
[InlineData(true)]
369-
[InlineData(false)]
370-
public async Task ObjectResult_WithStringReturnType_SetsMediaTypeToAccept(bool matchFormatterOnObjectType)
368+
[Fact]
369+
public async Task ObjectResult_WithStringReturnType_AndNonTextPlainMediaType_DoesNotReturnTextPlain()
371370
{
372371
// Arrange
373-
var targetUri = "http://localhost/FallbackOnTypeBasedMatch/ReturnString?matchFormatterOnObjectType=" +
374-
matchFormatterOnObjectType;
372+
var targetUri = "http://localhost/FallbackOnTypeBasedMatch/ReturnString";
375373
var request = new HttpRequestMessage(HttpMethod.Get, targetUri);
376374
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
377375

@@ -380,9 +378,9 @@ public async Task ObjectResult_WithStringReturnType_SetsMediaTypeToAccept(bool m
380378

381379
// Assert
382380
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
383-
Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType);
381+
Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType.ToString());
384382
var actualBody = await response.Content.ReadAsStringAsync();
385-
Assert.Equal("Hello World!", actualBody);
383+
Assert.Equal("\"Hello World!\"", actualBody);
386384
}
387385

388386
[Fact]
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Net;
5+
using System.Net.Http;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Mvc.Formatters.Xml;
9+
using Microsoft.AspNetCore.Testing.xunit;
10+
using Xunit;
11+
12+
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
13+
{
14+
/// <summary>
15+
/// These tests are for scenarios when <see cref="MvcOptions.RespectBrowserAcceptHeader"/> is <c>False</c>, which is the default.
16+
/// </summary>
17+
public class DoNotRespectBrowserAcceptHeaderTests : IClassFixture<MvcTestFixture<FormatterWebSite.Startup>>
18+
{
19+
public DoNotRespectBrowserAcceptHeaderTests(MvcTestFixture<FormatterWebSite.Startup> fixture)
20+
{
21+
Client = fixture.Client;
22+
}
23+
24+
public HttpClient Client { get; }
25+
26+
[Theory]
27+
[InlineData("application/xml,*/*;q=0.2")]
28+
[InlineData("application/xml,*/*")]
29+
public async Task AllMediaRangeAcceptHeader_FirstFormatterInListWritesResponse(string acceptHeader)
30+
{
31+
// Arrange
32+
var request = RequestWithAccept("http://localhost/DoNotRespectBrowserAcceptHeader/EmployeeInfo", acceptHeader);
33+
34+
// Act
35+
var response = await Client.SendAsync(request);
36+
37+
// Assert
38+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
39+
Assert.NotNull(response.Content);
40+
Assert.NotNull(response.Content.Headers.ContentType);
41+
Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType.ToString());
42+
var responseData = await response.Content.ReadAsStringAsync();
43+
Assert.Equal("{\"id\":10,\"name\":\"John\"}", responseData);
44+
}
45+
46+
[ConditionalTheory]
47+
// Mono issue - https://github.com/aspnet/External/issues/18
48+
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
49+
[InlineData("application/xml,*/*;q=0.2")]
50+
[InlineData("application/xml,*/*")]
51+
public async Task AllMediaRangeAcceptHeader_ProducesAttributeIsHonored(string acceptHeader)
52+
{
53+
// Arrange
54+
var request = RequestWithAccept(
55+
"http://localhost/DoNotRespectBrowserAcceptHeader/EmployeeInfoWithProduces",
56+
acceptHeader);
57+
var expectedResponseData =
58+
"<DoNotRespectBrowserAcceptHeaderController.Employee xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"" +
59+
" xmlns=\"http://schemas.datacontract.org/2004/07/FormatterWebSite.Controllers\"><Id>20</Id><Name>Mike" +
60+
"</Name></DoNotRespectBrowserAcceptHeaderController.Employee>";
61+
62+
// Act
63+
var response = await Client.SendAsync(request);
64+
65+
// Assert
66+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
67+
Assert.NotNull(response.Content);
68+
Assert.NotNull(response.Content.Headers.ContentType);
69+
Assert.Equal("application/xml; charset=utf-8", response.Content.Headers.ContentType.ToString());
70+
var responseData = await response.Content.ReadAsStringAsync();
71+
XmlAssert.Equal(expectedResponseData, responseData);
72+
}
73+
74+
[ConditionalTheory]
75+
// Mono issue - https://github.com/aspnet/External/issues/18
76+
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
77+
[InlineData("application/xml,*/*;q=0.2")]
78+
[InlineData("application/xml,*/*")]
79+
public async Task AllMediaRangeAcceptHeader_WithContentTypeHeader_ContentTypeIsIgnored(string acceptHeader)
80+
{
81+
// Arrange
82+
var requestData =
83+
"<DoNotRespectBrowserAcceptHeaderController.Employee xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"" +
84+
" xmlns=\"http://schemas.datacontract.org/2004/07/FormatterWebSite.Controllers\"><Id>35</Id><Name>Jimmy" +
85+
"</Name></DoNotRespectBrowserAcceptHeaderController.Employee>";
86+
var expectedResponseData = @"{""id"":35,""name"":""Jimmy""}";
87+
var request = RequestWithAccept("http://localhost/DoNotRespectBrowserAcceptHeader/CreateEmployee", acceptHeader);
88+
request.Content = new StringContent(requestData, Encoding.UTF8, "application/xml");
89+
request.Method = HttpMethod.Post;
90+
91+
// Act
92+
var response = await Client.SendAsync(request);
93+
94+
// Assert
95+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
96+
Assert.NotNull(response.Content);
97+
Assert.NotNull(response.Content.Headers.ContentType);
98+
99+
// Site uses default output formatter (ignores Accept header) because that header contained a wildcard match.
100+
Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType.ToString());
101+
102+
var responseData = await response.Content.ReadAsStringAsync();
103+
Assert.Equal(expectedResponseData, responseData);
104+
}
105+
106+
[ConditionalTheory]
107+
// Mono issue - https://github.com/aspnet/External/issues/18
108+
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
109+
[InlineData("application/xml,application/json;q=0.2")]
110+
[InlineData("application/xml,application/json")]
111+
public async Task AllMediaRangeAcceptHeader_WithExactMatch_ReturnsExpectedContent(string acceptHeader)
112+
{
113+
// Arrange
114+
var requestData =
115+
"<DoNotRespectBrowserAcceptHeaderController.Employee xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\"" +
116+
" xmlns=\"http://schemas.datacontract.org/2004/07/FormatterWebSite.Controllers\"><Id>35</Id><Name>Jimmy" +
117+
"</Name></DoNotRespectBrowserAcceptHeaderController.Employee>";
118+
var request = RequestWithAccept("http://localhost/DoNotRespectBrowserAcceptHeader/CreateEmployee", acceptHeader);
119+
request.Content = new StringContent(requestData, Encoding.UTF8, "application/xml");
120+
request.Method = HttpMethod.Post;
121+
122+
// Act
123+
var response = await Client.SendAsync(request);
124+
125+
// Assert
126+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
127+
Assert.NotNull(response.Content);
128+
Assert.NotNull(response.Content.Headers.ContentType);
129+
Assert.Equal("application/xml; charset=utf-8", response.Content.Headers.ContentType.ToString());
130+
var responseData = await response.Content.ReadAsStringAsync();
131+
Assert.Equal(requestData, responseData);
132+
}
133+
134+
private static HttpRequestMessage RequestWithAccept(string url, string accept)
135+
{
136+
var request = new HttpRequestMessage(HttpMethod.Get, url);
137+
request.Headers.Add("Accept", accept);
138+
139+
return request;
140+
}
141+
}
142+
}

0 commit comments

Comments
 (0)