diff --git a/src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs b/src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs
index c70920ce..850ed1f9 100644
--- a/src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs
@@ -466,6 +466,12 @@ public class ExternalSystemClient : IExternalSystemClient
/// into store-and-forward would defeat the cap. Cancellation
/// () is allowed to propagate so the
/// caller's ordered timeout/cancellation catch filters classify it.
+ ///
+ /// N4: the accumulated bytes are decoded using the response's declared charset
+ /// (Content-Type: …; charset=…) via — parity
+ /// with the pre-bounded-read ReadAsStringAsync. A null, empty, or
+ /// unrecognized charset falls back to UTF-8.
+ ///
///
private static async Task 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);
+ }
+
+ ///
+ /// Resolves the declared response charset to an , tolerating
+ /// the quoted form some servers emit (charset="iso-8859-1"). Null, empty,
+ /// or unrecognized values fall back to UTF-8.
+ ///
+ 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)
diff --git a/tests/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway.Tests/ExternalSystemClientTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway.Tests/ExternalSystemClientTests.cs
index fcbfe7c2..206c592f 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway.Tests/ExternalSystemClientTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway.Tests/ExternalSystemClientTests.cs
@@ -1,5 +1,6 @@
using System.Net;
using System.Net.Http.Headers;
+using System.Text;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -1377,4 +1378,57 @@ public class ExternalSystemClientTests
Assert.False(delivered);
}
+
+ [Fact]
+ public async Task Call_Latin1Charset_DecodesUsingDeclaredCharset()
+ {
+ // Arch-review R2 N4: the bounded read must honor the declared charset —
+ // UTF-8-decoding an iso-8859-1 body turns every non-ASCII byte into U+FFFD.
+ var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
+ var method = new ExternalSystemMethod("getData", "GET", "/data") { Id = 1, ExternalSystemDefinitionId = 1 };
+ StubResolution(system, method);
+ var handler = new ByteBodyHandler(
+ HttpStatusCode.OK,
+ Encoding.Latin1.GetBytes("température 25°C"),
+ "text/plain; charset=iso-8859-1");
+ _httpClientFactory.CreateClient(Arg.Any()).Returns(new HttpClient(handler));
+ var client = new ExternalSystemClient(
+ _httpClientFactory, _repository, NullLogger.Instance);
+
+ var result = await client.CallAsync("TestAPI", "getData");
+
+ Assert.True(result.Success);
+ Assert.Contains("température 25°C", result.ResponseJson);
+ }
+
+ [Fact]
+ public async Task Call_UnknownCharset_FallsBackToUtf8()
+ {
+ var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
+ var method = new ExternalSystemMethod("getData", "GET", "/data") { Id = 1, ExternalSystemDefinitionId = 1 };
+ StubResolution(system, method);
+ var handler = new ByteBodyHandler(
+ HttpStatusCode.OK, Encoding.UTF8.GetBytes("ok"), "text/plain; charset=not-a-charset");
+ _httpClientFactory.CreateClient(Arg.Any()).Returns(new HttpClient(handler));
+ var client = new ExternalSystemClient(
+ _httpClientFactory, _repository, NullLogger.Instance);
+
+ var result = await client.CallAsync("TestAPI", "getData");
+
+ Assert.True(result.Success);
+ Assert.Contains("ok", result.ResponseJson);
+ }
+
+ /// Test helper: returns a fixed byte body with an explicit Content-Type.
+ private sealed class ByteBodyHandler(
+ HttpStatusCode statusCode, byte[] body, string contentType) : HttpMessageHandler
+ {
+ protected override Task SendAsync(
+ HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ var response = new HttpResponseMessage(statusCode) { Content = new ByteArrayContent(body) };
+ response.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
+ return Task.FromResult(response);
+ }
+ }
}