Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid modifying the JavaScriptEncoder on SystemTextJsonOutputFormatter #39778

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System.Runtime.ExceptionServices;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;

namespace Microsoft.AspNetCore.Mvc.Formatters;
Expand All @@ -30,18 +29,7 @@ public SystemTextJsonOutputFormatter(JsonSerializerOptions jsonSerializerOptions

internal static SystemTextJsonOutputFormatter CreateFormatter(JsonOptions jsonOptions)
{
var jsonSerializerOptions = jsonOptions.JsonSerializerOptions;

if (jsonSerializerOptions.Encoder is null)
{
// If the user hasn't explicitly configured the encoder, use the less strict encoder that does not encode all non-ASCII characters.
jsonSerializerOptions = new JsonSerializerOptions(jsonSerializerOptions)
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
}

return new SystemTextJsonOutputFormatter(jsonSerializerOptions);
return new SystemTextJsonOutputFormatter(jsonOptions.JsonSerializerOptions);
}

/// <summary>
Expand Down
82 changes: 0 additions & 82 deletions src/Mvc/Mvc.Core/test/Formatters/JsonOutputFormatterTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,88 +56,6 @@ public void CanWriteResult_ReturnsExpectedValueForMediaType(
Assert.Equal(new StringSegment(expectedContentType), outputFormatterContext.ContentType);
}

public static TheoryData<string, string, bool> WriteCorrectCharacterEncoding
{
get
{
var data = new TheoryData<string, string, bool>
{
{ "This is a test 激光這兩個字是甚麼意思 string written using utf-8", "utf-8", true },
{ "This is a test 激光這兩個字是甚麼意思 string written using utf-16", "utf-16", true },
{ "This is a test 激光這兩個字是甚麼意思 string written using utf-32", "utf-32", false },
{ "This is a test æøå string written using iso-8859-1", "iso-8859-1", false },
};

return data;
}
}

[Theory]
[MemberData(nameof(WriteCorrectCharacterEncoding))]
public async Task WriteToStreamAsync_UsesCorrectCharacterEncoding(
string content,
string encodingAsString,
bool isDefaultEncoding)
{
// Arrange
var formatter = GetOutputFormatter();
var expectedContent = "\"" + content + "\"";
var mediaType = MediaTypeHeaderValue.Parse(string.Format(CultureInfo.InvariantCulture, "application/json; charset={0}", encodingAsString));
var encoding = CreateOrGetSupportedEncoding(formatter, encodingAsString, isDefaultEncoding);


var body = new MemoryStream();
var actionContext = GetActionContext(mediaType, body);

var outputFormatterContext = new OutputFormatterWriteContext(
actionContext.HttpContext,
new TestHttpResponseStreamWriterFactory().CreateWriter,
typeof(string),
content)
{
ContentType = new StringSegment(mediaType.ToString()),
};

// Act
await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding(encodingAsString));

// Assert
var actualContent = encoding.GetString(body.ToArray());
Assert.Equal(expectedContent, actualContent, StringComparer.OrdinalIgnoreCase);
Assert.True(body.CanWrite, "Response body should not be disposed.");
}

[Fact]
public async Task WriteResponseBodyAsync_Encodes()
{
// Arrange
var formatter = GetOutputFormatter();
var expectedContent = "{\"key\":\"Hello \\n <b>Wörld</b>\"}";
var content = new { key = "Hello \n <b>Wörld</b>" };

var mediaType = MediaTypeHeaderValue.Parse("application/json; charset=utf-8");
var encoding = CreateOrGetSupportedEncoding(formatter, "utf-8", isDefaultEncoding: true);

var body = new MemoryStream();
var actionContext = GetActionContext(mediaType, body);

var outputFormatterContext = new OutputFormatterWriteContext(
actionContext.HttpContext,
new TestHttpResponseStreamWriterFactory().CreateWriter,
typeof(string),
content)
{
ContentType = new StringSegment(mediaType.ToString()),
};

// Act
await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding("utf-8"));

// Assert
var actualContent = encoding.GetString(body.ToArray());
Assert.Equal(expectedContent, actualContent);
}

[Fact]
public async Task ErrorDuringSerialization_DoesNotCloseTheBrackets()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Primitives;
Expand Down Expand Up @@ -54,6 +55,76 @@ public async Task WriteResponseBodyAsync_AllowsConfiguringPreserveReferenceHandl
Assert.Equal(expectedContent, actualContent);
}

[Fact]
public async Task WriteResponseBodyAsync_Encodes()
{
// Arrange
var formatter = GetOutputFormatter();

var expectedContent = "{\"key\":\"Hello \\n \\u003Cb\\u003EW\\u00F6rld\\u003C/b\\u003E\"}";
var content = new { key = "Hello \n <b>Wörld</b>" };

var mediaType = MediaTypeHeaderValue.Parse("application/json; charset=utf-8");
var encoding = CreateOrGetSupportedEncoding(formatter, "utf-8", isDefaultEncoding: true);

var body = new MemoryStream();
var actionContext = GetActionContext(mediaType, body);

var outputFormatterContext = new OutputFormatterWriteContext(
actionContext.HttpContext,
new TestHttpResponseStreamWriterFactory().CreateWriter,
typeof(string),
content)
{
ContentType = new StringSegment(mediaType.ToString()),
};

// Act
await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding("utf-8"));

// Assert
var actualContent = encoding.GetString(body.ToArray());
Assert.Equal(expectedContent, actualContent);
}

[Fact]
public async Task WriteResponseBodyAsync_WithUnsafeRelaxedEncoding_Encodes()
{
// Arrange
var formatter = SystemTextJsonOutputFormatter.CreateFormatter(new()
{
JsonSerializerOptions =
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
},
});

var expectedContent = "{\"key\":\"Hello \\n <b>Wörld</b>\"}";
var content = new { key = "Hello \n <b>Wörld</b>" };

var mediaType = MediaTypeHeaderValue.Parse("application/json; charset=utf-8");
var encoding = CreateOrGetSupportedEncoding(formatter, "utf-8", isDefaultEncoding: true);

var body = new MemoryStream();
var actionContext = GetActionContext(mediaType, body);

var outputFormatterContext = new OutputFormatterWriteContext(
actionContext.HttpContext,
new TestHttpResponseStreamWriterFactory().CreateWriter,
typeof(string),
content)
{
ContentType = new StringSegment(mediaType.ToString()),
};

// Act
await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding("utf-8"));

// Assert
var actualContent = encoding.GetString(body.ToArray());
Assert.Equal(expectedContent, actualContent);
}

[Fact]
public async Task WriteResponseBodyAsync_WithNonUtf8Encoding_FormattingErrorsAreThrown()
{
Expand Down Expand Up @@ -156,6 +227,56 @@ async IAsyncEnumerable<int> AsyncEnumerableClosedConnection([EnumeratorCancellat
}
}

public static TheoryData<string, string, bool> WriteCorrectCharacterEncoding
{
get
{
var data = new TheoryData<string, string, bool>
{
{ "This is a test 激光這兩個字是甚麼意思 string written using utf-8", "utf-8", true },
{ "This is a test 激光這兩個字是甚麼意思 string written using utf-16", "utf-16", true },
{ "This is a test 激光這兩個字是甚麼意思 string written using utf-32", "utf-32", false },
{ "This is a test æøå string written using iso-8859-1", "iso-8859-1", false },
};

return data;
}
}

[Theory]
[MemberData(nameof(WriteCorrectCharacterEncoding))]
public async Task WriteToStreamAsync_UsesCorrectCharacterEncoding(
string content,
string encodingAsString,
bool isDefaultEncoding)
{
// Arrange
var formatter = GetOutputFormatter();
var expectedContent = "\"" + JavaScriptEncoder.Default.Encode(content) + "\"";
var mediaType = MediaTypeHeaderValue.Parse($"application/json; charset={encodingAsString}");
var encoding = CreateOrGetSupportedEncoding(formatter, encodingAsString, isDefaultEncoding);

var body = new MemoryStream();
var actionContext = GetActionContext(mediaType, body);

var outputFormatterContext = new OutputFormatterWriteContext(
actionContext.HttpContext,
new TestHttpResponseStreamWriterFactory().CreateWriter,
typeof(string),
content)
{
ContentType = new StringSegment(mediaType.ToString()),
};

// Act
await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding(encodingAsString));

// Assert
var actualContent = encoding.GetString(body.ToArray());
Assert.Equal(expectedContent, actualContent, StringComparer.OrdinalIgnoreCase);
Assert.True(body.CanWrite, "Response body should not be disposed.");
}

private class Person
{
public string Name { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,37 @@ public async Task MvcJsonOptionsAreUsedToSetBufferThreshold()
Assert.Equal(2, ((FileBufferingWriteStream)writeStream).MemoryThreshold);
}

[Fact]
public async Task WriteResponseBodyAsync_Encodes()
{
// Arrange
var formatter = GetOutputFormatter();
var expectedContent = "{\"key\":\"Hello \\n <b>Wörld</b>\"}";
var content = new { key = "Hello \n <b>Wörld</b>" };

var mediaType = MediaTypeHeaderValue.Parse("application/json; charset=utf-8");
var encoding = CreateOrGetSupportedEncoding(formatter, "utf-8", isDefaultEncoding: true);

var body = new MemoryStream();
var actionContext = GetActionContext(mediaType, body);

var outputFormatterContext = new OutputFormatterWriteContext(
actionContext.HttpContext,
new TestHttpResponseStreamWriterFactory().CreateWriter,
typeof(string),
content)
{
ContentType = new StringSegment(mediaType.ToString()),
};

// Act
await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding("utf-8"));

// Assert
var actualContent = encoding.GetString(body.ToArray());
Assert.Equal(expectedContent, actualContent);
}

[Fact]
public async Task ChangesTo_SerializerSettings_AffectSerialization()
{
Expand Down Expand Up @@ -549,6 +580,56 @@ async IAsyncEnumerable<int> AsyncEnumerableThrows([EnumeratorCancellation] Cance
}
}

public static TheoryData<string, string, bool> WriteCorrectCharacterEncoding
{
get
{
var data = new TheoryData<string, string, bool>
{
{ "This is a test 激光這兩個字是甚麼意思 string written using utf-8", "utf-8", true },
{ "This is a test 激光這兩個字是甚麼意思 string written using utf-16", "utf-16", true },
{ "This is a test 激光這兩個字是甚麼意思 string written using utf-32", "utf-32", false },
{ "This is a test æøå string written using iso-8859-1", "iso-8859-1", false },
};

return data;
}
}

[Theory]
[MemberData(nameof(WriteCorrectCharacterEncoding))]
public async Task WriteToStreamAsync_UsesCorrectCharacterEncoding(
string content,
string encodingAsString,
bool isDefaultEncoding)
{
// Arrange
var formatter = GetOutputFormatter();
var expectedContent = "\"" + content + "\"";
var mediaType = MediaTypeHeaderValue.Parse($"application/json; charset={encodingAsString}");
var encoding = CreateOrGetSupportedEncoding(formatter, encodingAsString, isDefaultEncoding);

var body = new MemoryStream();
var actionContext = GetActionContext(mediaType, body);

var outputFormatterContext = new OutputFormatterWriteContext(
actionContext.HttpContext,
new TestHttpResponseStreamWriterFactory().CreateWriter,
typeof(string),
content)
{
ContentType = new StringSegment(mediaType.ToString()),
};

// Act
await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding(encodingAsString));

// Assert
var actualContent = encoding.GetString(body.ToArray());
Assert.Equal(expectedContent, actualContent, StringComparer.OrdinalIgnoreCase);
Assert.True(body.CanWrite, "Response body should not be disposed.");
}

private class TestableJsonOutputFormatter : NewtonsoftJsonOutputFormatter
{
public TestableJsonOutputFormatter(JsonSerializerSettings serializerSettings)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net;
Expand Down Expand Up @@ -105,7 +105,7 @@ public virtual async Task Formatting_StringValueWithNonAsciiCharacters()

// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
Assert.Equal("\"Une bête de cirque\"", await response.Content.ReadAsStringAsync());
Assert.Equal("\"Une b\\u00EAte de cirque\"", await response.Content.ReadAsStringAsync());
}

[Fact]
Expand Down
Loading