diff --git a/CHANGELOG.md b/CHANGELOG.md index 65d9b28..e87e45f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * redfish.py: a cached Redfish session token is now kept only as long as the controller itself keeps the session alive, read from the controller's own session timeout. This avoids sporadic "401 Unauthorized" errors on controllers with short session timeouts (such as Supermicro's 300 seconds) without having to tune the cache expiration by hand ([#246](https://github.com/Linuxfabrik/lib/issues/246)). * time.py: clarified the `timestr2epoch(..., pattern='iso8601')` docstring - the mode is backed by `datetime.fromisoformat()`, not a full ISO 8601 parser, and which layouts it accepts beyond RFC 3339 depends on the Python version. +### Fixed + +* url.py: fetching a page or JSON no longer fails with a decode error when the remote host sends non-UTF-8 content without declaring a charset (such as sensor firmware that serves the degree sign as a raw Latin-1 byte). The response is read as Latin-1 in that case instead of aborting the check. + ## [v5.1.0] - 2026-06-24 diff --git a/tests/url/unit-test/run b/tests/url/unit-test/run index 92473b6..809f274 100755 --- a/tests/url/unit-test/run +++ b/tests/url/unit-test/run @@ -476,6 +476,18 @@ class TestFetchSuccess(unittest.TestCase): self.assertTrue(ok) self.assertEqual(res, 'über') + def test_undeclared_charset_falls_back_to_latin1(self): + # Comet System Web Sensors serve ° as the raw Latin-1 byte 0xb0 and + # declare no charset. Strict UTF-8 would raise on 0xb0; without a + # declared charset lib.url must fall back to Latin-1 rather than fail. + fake, _ = make_fake_client( + FakeStreamResponse(body=b'25.3 \xb0C', charset_encoding=None) + ) + with patch_client(fake): + ok, res = url.fetch('https://example.com') + self.assertTrue(ok) + self.assertEqual(res, '25.3 °C') + def test_no_proxy_disables_trust_env(self): fake, cap = make_fake_client(FakeStreamResponse()) with patch_client(fake): diff --git a/url.py b/url.py index f72333d..db282e1 100644 --- a/url.py +++ b/url.py @@ -11,7 +11,7 @@ """Get for example HTML or JSON from an URL.""" __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland' -__version__ = '2026061901' +__version__ = '2026070301' import base64 import json @@ -537,7 +537,21 @@ def fetch( try: charset = response_charset or 'UTF-8' - body_decoded = body_bytes.decode(charset) if to_text else body_bytes + if to_text: + try: + body_decoded = body_bytes.decode(charset) + except UnicodeDecodeError: + if response_charset: + # The server explicitly declared this charset, so a mismatch + # is a genuine error and must surface to the caller. + raise + # No charset header was sent and our UTF-8 assumption was wrong. + # Latin-1 maps every byte 1:1 and never fails, preserving bytes + # like 0xb0 (° in ISO-8859-1) emitted by sensor firmware that + # serves non-UTF-8 content without a charset header. + body_decoded = body_bytes.decode('latin-1') + else: + body_decoded = body_bytes if not extended: return success, body_decoded