Fix Content-Type header charset auto-appending in HTTP requests (#6676)
* Initial plan for issue * Implement RawStringContent for fixing charset issue Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
This commit is contained in:
parent
830a04f325
commit
144edc3a85
9
Elsa.sln
9
Elsa.sln
|
|
@ -1,4 +1,4 @@
|
|||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.7.34003.232
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
|
|
@ -262,6 +262,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Expressions.Python", "
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.SasTokens", "src\modules\Elsa.SasTokens\Elsa.SasTokens.csproj", "{A7DE02E3-405B-B6BA-7E47-9E27D4BEFB24}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Http.UnitTests", "test\unit\Elsa.Http.UnitTests\Elsa.Http.UnitTests.csproj", "{66ACA2B3-DB48-4F68-B26D-62555F3CE69B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -450,6 +452,10 @@ Global
|
|||
{A7DE02E3-405B-B6BA-7E47-9E27D4BEFB24}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A7DE02E3-405B-B6BA-7E47-9E27D4BEFB24}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A7DE02E3-405B-B6BA-7E47-9E27D4BEFB24}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{66ACA2B3-DB48-4F68-B26D-62555F3CE69B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{66ACA2B3-DB48-4F68-B26D-62555F3CE69B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{66ACA2B3-DB48-4F68-B26D-62555F3CE69B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{66ACA2B3-DB48-4F68-B26D-62555F3CE69B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -527,6 +533,7 @@ Global
|
|||
{6AF53651-99F0-1DE0-D37B-4FF6B0348DBB} = {6EF07978-A6D2-40EB-891D-7D70C5F37E76}
|
||||
{9D8FB664-88B4-10BE-58A2-D9A1644AD2E4} = {6EF07978-A6D2-40EB-891D-7D70C5F37E76}
|
||||
{A7DE02E3-405B-B6BA-7E47-9E27D4BEFB24} = {5948B0A5-7873-4DBB-BA03-EB283D6EA91B}
|
||||
{66ACA2B3-DB48-4F68-B26D-62555F3CE69B} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,6 @@ public class JsonContentFactory : IHttpContentFactory
|
|||
if (string.IsNullOrWhiteSpace(contentType))
|
||||
contentType = MediaTypeNames.Application.Json;
|
||||
|
||||
return new StringContent(text, Encoding.UTF8, contentType);
|
||||
return new RawStringContent(text, Encoding.UTF8, contentType);
|
||||
}
|
||||
}
|
||||
48
src/modules/Elsa.Http/ContentWriters/RawStringContent.cs
Normal file
48
src/modules/Elsa.Http/ContentWriters/RawStringContent.cs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
|
||||
namespace Elsa.Http.ContentWriters;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="HttpContent"/> implementation that allows setting the content type without automatically appending charset information.
|
||||
/// </summary>
|
||||
public class RawStringContent : HttpContent
|
||||
{
|
||||
private readonly string _content;
|
||||
private readonly Encoding _encoding;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="RawStringContent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="content">The content to send.</param>
|
||||
/// <param name="encoding">The encoding to use when sending the content.</param>
|
||||
/// <param name="mediaType">The media type to use for the content.</param>
|
||||
public RawStringContent(string content, Encoding encoding, string mediaType)
|
||||
{
|
||||
_content = content;
|
||||
_encoding = encoding;
|
||||
|
||||
// Set the media type exactly as provided without appending charset information
|
||||
Headers.ContentType = new MediaTypeHeaderValue(mediaType);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) =>
|
||||
SerializeToStreamAsync(stream, context, CancellationToken.None);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken)
|
||||
{
|
||||
using var writer = new StreamWriter(stream, _encoding, leaveOpen: true);
|
||||
await writer.WriteAsync(_content.AsMemory(), cancellationToken);
|
||||
await writer.FlushAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool TryComputeLength(out long length)
|
||||
{
|
||||
length = _encoding.GetByteCount(_content);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,6 @@ public class TextContentFactory : IHttpContentFactory
|
|||
if (string.IsNullOrWhiteSpace(contentType))
|
||||
contentType = MediaTypeNames.Text.Plain;
|
||||
|
||||
return new StringContent(text!, Encoding.UTF8, contentType);
|
||||
return new RawStringContent(text!, Encoding.UTF8, contentType);
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ public class XmlContentFactory : IHttpContentFactory
|
|||
public HttpContent CreateHttpContent(object content, string contentType)
|
||||
{
|
||||
var text = content as string ?? Serialize(content);
|
||||
return new StringContent(text, Encoding.UTF8, contentType);
|
||||
return new RawStringContent(text, Encoding.UTF8, contentType);
|
||||
}
|
||||
|
||||
private string Serialize(object value)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
using Elsa.Http.ContentWriters;
|
||||
using Xunit;
|
||||
|
||||
namespace Elsa.Http.UnitTests.ContentWriters;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="IHttpContentFactory"/> implementations.
|
||||
/// </summary>
|
||||
public class ContentFactoryTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests that <see cref="JsonContentFactory"/> doesn't append charset to content type.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void JsonContentFactory_ShouldNotAppendCharset()
|
||||
{
|
||||
// Arrange
|
||||
const string contentType = "application/json";
|
||||
const string content = "{\"test\": \"value\"}";
|
||||
var factory = new JsonContentFactory();
|
||||
|
||||
// Act
|
||||
var httpContent = factory.CreateHttpContent(content, contentType);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(contentType, httpContent.Headers.ContentType?.MediaType);
|
||||
Assert.Null(httpContent.Headers.ContentType?.CharSet);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that <see cref="XmlContentFactory"/> doesn't append charset to content type.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void XmlContentFactory_ShouldNotAppendCharset()
|
||||
{
|
||||
// Arrange
|
||||
const string contentType = "text/xml";
|
||||
const string content = "<root>test</root>";
|
||||
var factory = new XmlContentFactory();
|
||||
|
||||
// Act
|
||||
var httpContent = factory.CreateHttpContent(content, contentType);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(contentType, httpContent.Headers.ContentType?.MediaType);
|
||||
Assert.Null(httpContent.Headers.ContentType?.CharSet);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that <see cref="TextContentFactory"/> doesn't append charset to content type.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TextContentFactory_ShouldNotAppendCharset()
|
||||
{
|
||||
// Arrange
|
||||
const string contentType = "text/html";
|
||||
const string content = "<html><body>test</body></html>";
|
||||
var factory = new TextContentFactory();
|
||||
|
||||
// Act
|
||||
var httpContent = factory.CreateHttpContent(content, contentType);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(contentType, httpContent.Headers.ContentType?.MediaType);
|
||||
Assert.Null(httpContent.Headers.ContentType?.CharSet);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using Elsa.Http.ContentWriters;
|
||||
using Xunit;
|
||||
|
||||
namespace Elsa.Http.UnitTests.ContentWriters;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <see cref="RawStringContent"/> class.
|
||||
/// </summary>
|
||||
public class RawStringContentTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests that the content type is set exactly as provided without appending charset information.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ContentType_ShouldNotAppendCharset()
|
||||
{
|
||||
// Arrange
|
||||
const string contentType = "text/xml";
|
||||
const string content = "<root>test</root>";
|
||||
|
||||
// Act
|
||||
var rawContent = new RawStringContent(content, Encoding.UTF8, contentType);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(contentType, rawContent.Headers.ContentType?.MediaType);
|
||||
Assert.Null(rawContent.Headers.ContentType?.CharSet);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that the content type with parameters is preserved exactly as provided.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ContentType_WithParameters_ShouldPreserveParameters()
|
||||
{
|
||||
// Arrange
|
||||
const string contentType = "application/json; custom-param=value";
|
||||
var expectedMediaType = new MediaTypeHeaderValue(contentType);
|
||||
const string content = "{\"test\": \"value\"}";
|
||||
|
||||
// Act
|
||||
var rawContent = new RawStringContent(content, Encoding.UTF8, contentType);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedMediaType.MediaType, rawContent.Headers.ContentType?.MediaType);
|
||||
Assert.Equal(expectedMediaType.Parameters.Count(), rawContent.Headers.ContentType?.Parameters.Count());
|
||||
|
||||
var expectedParam = expectedMediaType.Parameters.First();
|
||||
var actualParam = rawContent.Headers.ContentType?.Parameters.First();
|
||||
|
||||
Assert.Equal(expectedParam.Name, actualParam?.Name);
|
||||
Assert.Equal(expectedParam.Value, actualParam?.Value);
|
||||
}
|
||||
}
|
||||
30
test/unit/Elsa.Http.UnitTests/Elsa.Http.UnitTests.csproj
Normal file
30
test/unit/Elsa.Http.UnitTests/Elsa.Http.UnitTests.csproj
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="xunit" Version="2.7.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\modules\Elsa.Http\Elsa.Http.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Loading…
Reference in a new issue