Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 12 additions & 0 deletions tests/url/unit-test/run
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
18 changes: 16 additions & 2 deletions url.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down