fix(esg): honor the declared response charset in ReadBodyBoundedAsync with UTF-8 fallback (plan R2-06 T4)

This commit is contained in:
Joseph Doherty
2026-07-13 09:53:00 -04:00
parent cc94b3a68c
commit b41b3b1b31
2 changed files with 87 additions and 1 deletions
@@ -466,6 +466,12 @@ public class ExternalSystemClient : IExternalSystemClient
/// into store-and-forward would defeat the cap. Cancellation
/// (<see cref="OperationCanceledException"/>) is allowed to propagate so the
/// caller's ordered timeout/cancellation catch filters classify it.
/// <para>
/// N4: the accumulated bytes are decoded using the response's declared charset
/// (<c>Content-Type: …; charset=…</c>) via <see cref="ResolveEncoding"/> — parity
/// with the pre-bounded-read <c>ReadAsStringAsync</c>. A null, empty, or
/// unrecognized charset falls back to UTF-8.
/// </para>
/// </summary>
private static async Task<string> ReadBodyBoundedAsync(
HttpContent content, string systemName, long maxBytes, CancellationToken token)
@@ -498,7 +504,33 @@ public class ExternalSystemClient : IExternalSystemClient
}
}
return Encoding.UTF8.GetString(buffer.GetBuffer(), 0, (int)buffer.Length);
// N4: honor the response's declared charset (parity with the pre-bounded-read
// ReadAsStringAsync). Unknown/invalid charset → UTF-8 fallback: mojibake beats
// failing the call over a cosmetic header.
return ResolveEncoding(content.Headers.ContentType?.CharSet)
.GetString(buffer.GetBuffer(), 0, (int)buffer.Length);
}
/// <summary>
/// Resolves the declared response charset to an <see cref="Encoding"/>, tolerating
/// the quoted form some servers emit (<c>charset="iso-8859-1"</c>). Null, empty,
/// or unrecognized values fall back to UTF-8.
/// </summary>
private static Encoding ResolveEncoding(string? charset)
{
if (string.IsNullOrWhiteSpace(charset))
{
return Encoding.UTF8;
}
try
{
return Encoding.GetEncoding(charset.Trim().Trim('"'));
}
catch (ArgumentException)
{
return Encoding.UTF8;
}
}
private static string Truncate(string value, int maxChars)