From 2e2d7a4e3fc3f303144bdc55db6557aa2ef2f3cf Mon Sep 17 00:00:00 2001 From: Mike Rotondo Date: Tue, 16 Jun 2026 11:38:30 +0200 Subject: [PATCH] Strip surrounding quotes from charset before Encoding.GetEncoding A quoted charset parameter (e.g. charset="utf-8") is valid per RFC 7231 3.1.1.1, and System.Net.Http's MediaTypeHeaderValue.CharSet returns the value with the quotes intact. Passing it straight to Encoding.GetEncoding throws ArgumentException: '"utf-8"' is not a supported encoding name. Real-world servers send this: IRS MeF (Apache/Axiom) emits application/xop+xml; charset="utf-8" on the MTOM root part, which crashes decoding in both MtomPart.GetStringContentForEncoder and MtomMessageEncoder.CreateStream. Trim the surrounding quotes (mirroring the existing handling of the type parameter) and guard against an absent charset. Adds an xUnit theory covering quoted (lower/upper), unquoted, and absent charset. Co-Authored-By: Claude Opus 4.8 --- .../MtomCharsetTests.cs | 59 +++++++++++++++++++ .../WcfCoreMtomEncoder.Tests.csproj | 26 ++++++++ src/WcfCoreMtomEncoder.sln | 26 ++++++++ src/WcfCoreMtomEncoder/MtomMessageEncoder.cs | 5 +- src/WcfCoreMtomEncoder/MtomPart.cs | 3 +- 5 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 src/WcfCoreMtomEncoder.Tests/MtomCharsetTests.cs create mode 100644 src/WcfCoreMtomEncoder.Tests/WcfCoreMtomEncoder.Tests.csproj diff --git a/src/WcfCoreMtomEncoder.Tests/MtomCharsetTests.cs b/src/WcfCoreMtomEncoder.Tests/MtomCharsetTests.cs new file mode 100644 index 0000000..eb1f2cc --- /dev/null +++ b/src/WcfCoreMtomEncoder.Tests/MtomCharsetTests.cs @@ -0,0 +1,59 @@ +using System.IO; +using System.ServiceModel.Channels; +using System.Text; +using Xunit; + +namespace WcfCoreMtomEncoder.Tests +{ + public class MtomCharsetTests + { + // Regression coverage for the charset parameter on the application/xop+xml root part. + // A quoted charset (e.g. charset="utf-8") is valid per RFC 7231 3.1.1.1 and is what IRS MeF + // sends; before the fix, GetEncoding was handed the value with the quotes still attached and + // threw: System.ArgumentException: '"utf-8"' is not a supported encoding name. + [Theory] + [InlineData("charset=\"utf-8\"")] // quoted, lowercase -> the reported crash + [InlineData("charset=\"UTF-8\"")] // quoted, uppercase + [InlineData("charset=utf-8")] // unquoted -> must keep working + [InlineData("")] // absent -> must fall back, not NRE + public void ReadMessage_decodes_xop_part_regardless_of_charset_quoting(string charsetParameter) + { + var message = ReadMtomResponse(charsetParameter); + + Assert.NotNull(message); + Assert.Equal("Ping", message.GetReaderAtBodyContents().LocalName); + } + + // Builds a minimal multipart/related MTOM response whose application/xop+xml root part carries + // the given charset token, then decodes it through MtomMessageEncoder (the path GenTax takes). + private static Message ReadMtomResponse(string charsetParameter) + { + MessageEncoder inner = new TextMessageEncodingBindingElement(MessageVersion.Soap11, Encoding.UTF8) + .CreateMessageEncoderFactory().Encoder; + var encoder = new MtomMessageEncoder(inner); + + const string boundary = "MIMEBoundary_test"; + var httpContentType = + $"multipart/related; boundary=\"{boundary}\"; type=\"application/xop+xml\"; start=\"\""; + + var partContentType = "application/xop+xml" + + (string.IsNullOrEmpty(charsetParameter) ? "" : "; " + charsetParameter) + + "; type=\"text/xml\""; + + var body = + $"--{boundary}\r\n" + + "Content-ID: \r\n" + + $"Content-Type: {partContentType}\r\n" + + "Content-Transfer-Encoding: binary\r\n" + + "\r\n" + + "" + + "\r\n" + + $"--{boundary}--\r\n"; + + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(body))) + { + return encoder.ReadMessage(stream, int.MaxValue, httpContentType); + } + } + } +} diff --git a/src/WcfCoreMtomEncoder.Tests/WcfCoreMtomEncoder.Tests.csproj b/src/WcfCoreMtomEncoder.Tests/WcfCoreMtomEncoder.Tests.csproj new file mode 100644 index 0000000..9f6129c --- /dev/null +++ b/src/WcfCoreMtomEncoder.Tests/WcfCoreMtomEncoder.Tests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/WcfCoreMtomEncoder.sln b/src/WcfCoreMtomEncoder.sln index 8903faf..69eacaa 100644 --- a/src/WcfCoreMtomEncoder.sln +++ b/src/WcfCoreMtomEncoder.sln @@ -5,16 +5,42 @@ VisualStudioVersion = 16.0.28803.156 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WcfCoreMtomEncoder", "WcfCoreMtomEncoder\WcfCoreMtomEncoder.csproj", "{F5F35855-99CB-44E0-9471-BC8BDE86B555}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WcfCoreMtomEncoder.Tests", "WcfCoreMtomEncoder.Tests\WcfCoreMtomEncoder.Tests.csproj", "{53F07002-4799-4535-BE67-B2CEFDB7A50D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {F5F35855-99CB-44E0-9471-BC8BDE86B555}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F5F35855-99CB-44E0-9471-BC8BDE86B555}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5F35855-99CB-44E0-9471-BC8BDE86B555}.Debug|x64.ActiveCfg = Debug|Any CPU + {F5F35855-99CB-44E0-9471-BC8BDE86B555}.Debug|x64.Build.0 = Debug|Any CPU + {F5F35855-99CB-44E0-9471-BC8BDE86B555}.Debug|x86.ActiveCfg = Debug|Any CPU + {F5F35855-99CB-44E0-9471-BC8BDE86B555}.Debug|x86.Build.0 = Debug|Any CPU {F5F35855-99CB-44E0-9471-BC8BDE86B555}.Release|Any CPU.ActiveCfg = Release|Any CPU {F5F35855-99CB-44E0-9471-BC8BDE86B555}.Release|Any CPU.Build.0 = Release|Any CPU + {F5F35855-99CB-44E0-9471-BC8BDE86B555}.Release|x64.ActiveCfg = Release|Any CPU + {F5F35855-99CB-44E0-9471-BC8BDE86B555}.Release|x64.Build.0 = Release|Any CPU + {F5F35855-99CB-44E0-9471-BC8BDE86B555}.Release|x86.ActiveCfg = Release|Any CPU + {F5F35855-99CB-44E0-9471-BC8BDE86B555}.Release|x86.Build.0 = Release|Any CPU + {53F07002-4799-4535-BE67-B2CEFDB7A50D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53F07002-4799-4535-BE67-B2CEFDB7A50D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53F07002-4799-4535-BE67-B2CEFDB7A50D}.Debug|x64.ActiveCfg = Debug|Any CPU + {53F07002-4799-4535-BE67-B2CEFDB7A50D}.Debug|x64.Build.0 = Debug|Any CPU + {53F07002-4799-4535-BE67-B2CEFDB7A50D}.Debug|x86.ActiveCfg = Debug|Any CPU + {53F07002-4799-4535-BE67-B2CEFDB7A50D}.Debug|x86.Build.0 = Debug|Any CPU + {53F07002-4799-4535-BE67-B2CEFDB7A50D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53F07002-4799-4535-BE67-B2CEFDB7A50D}.Release|Any CPU.Build.0 = Release|Any CPU + {53F07002-4799-4535-BE67-B2CEFDB7A50D}.Release|x64.ActiveCfg = Release|Any CPU + {53F07002-4799-4535-BE67-B2CEFDB7A50D}.Release|x64.Build.0 = Release|Any CPU + {53F07002-4799-4535-BE67-B2CEFDB7A50D}.Release|x86.ActiveCfg = Release|Any CPU + {53F07002-4799-4535-BE67-B2CEFDB7A50D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/WcfCoreMtomEncoder/MtomMessageEncoder.cs b/src/WcfCoreMtomEncoder/MtomMessageEncoder.cs index 49f8411..059254d 100644 --- a/src/WcfCoreMtomEncoder/MtomMessageEncoder.cs +++ b/src/WcfCoreMtomEncoder/MtomMessageEncoder.cs @@ -128,8 +128,9 @@ where ReferenceMatch(reference.Attribute("href"), part) private static Stream CreateStream(string content, MediaTypeHeaderValue contentType) { - var encoding = !string.IsNullOrEmpty(contentType.CharSet) - ? Encoding.GetEncoding(contentType.CharSet) + var charset = contentType.CharSet?.Trim('"'); + var encoding = !string.IsNullOrEmpty(charset) + ? Encoding.GetEncoding(charset) : Encoding.Default; return new MemoryStream(encoding.GetBytes(content)); diff --git a/src/WcfCoreMtomEncoder/MtomPart.cs b/src/WcfCoreMtomEncoder/MtomPart.cs index 4ea86b4..35f26c2 100644 --- a/src/WcfCoreMtomEncoder/MtomPart.cs +++ b/src/WcfCoreMtomEncoder/MtomPart.cs @@ -50,7 +50,8 @@ public string GetStringContentForEncoder(MessageEncoder encoder) throw new NotSupportedException(); } - var encoding = ContentType.CharSet != null ? Encoding.GetEncoding(ContentType.CharSet) : Encoding.Default; + var charset = ContentType.CharSet?.Trim('"'); + var encoding = !string.IsNullOrEmpty(charset) ? Encoding.GetEncoding(charset) : Encoding.Default; return encoding.GetString(GetRawContent()); }