Skip to content

Commit 9650727

Browse files
bdracofrenck
authored andcommitted
Fix REST sensor charset handling to respect Content-Type header (home-assistant#148223)
1 parent c965da6 commit 9650727

File tree

3 files changed

+114
-4
lines changed

3 files changed

+114
-4
lines changed

homeassistant/components/rest/data.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,14 @@ async def async_update(self, log_errors: bool = True) -> None:
140140
self._method, self._resource, **request_kwargs
141141
) as response:
142142
# Read the response
143-
self.data = await response.text(encoding=self._encoding)
143+
# Only use configured encoding if no charset in Content-Type header
144+
# If charset is present in Content-Type, let aiohttp use it
145+
if response.charset:
146+
# Let aiohttp use the charset from Content-Type header
147+
self.data = await response.text()
148+
else:
149+
# Use configured encoding as fallback
150+
self.data = await response.text(encoding=self._encoding)
144151
self.headers = response.headers
145152

146153
except TimeoutError as ex:

tests/components/rest/test_sensor.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,94 @@ async def test_setup_encoding(
162162
assert hass.states.get("sensor.mysensor").state == "tack själv"
163163

164164

165+
async def test_setup_auto_encoding_from_content_type(
166+
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
167+
) -> None:
168+
"""Test setup with encoding auto-detected from Content-Type header."""
169+
# Test with ISO-8859-1 charset in Content-Type header
170+
aioclient_mock.get(
171+
"http://localhost",
172+
status=HTTPStatus.OK,
173+
content="Björk Guðmundsdóttir".encode("iso-8859-1"),
174+
headers={"Content-Type": "text/plain; charset=iso-8859-1"},
175+
)
176+
assert await async_setup_component(
177+
hass,
178+
SENSOR_DOMAIN,
179+
{
180+
SENSOR_DOMAIN: {
181+
"name": "mysensor",
182+
# encoding defaults to UTF-8, but should be ignored when charset present
183+
"platform": DOMAIN,
184+
"resource": "http://localhost",
185+
"method": "GET",
186+
}
187+
},
188+
)
189+
await hass.async_block_till_done()
190+
assert len(hass.states.async_all(SENSOR_DOMAIN)) == 1
191+
assert hass.states.get("sensor.mysensor").state == "Björk Guðmundsdóttir"
192+
193+
194+
async def test_setup_encoding_fallback_no_charset(
195+
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
196+
) -> None:
197+
"""Test that configured encoding is used when no charset in Content-Type."""
198+
# No charset in Content-Type header
199+
aioclient_mock.get(
200+
"http://localhost",
201+
status=HTTPStatus.OK,
202+
content="Björk Guðmundsdóttir".encode("iso-8859-1"),
203+
headers={"Content-Type": "text/plain"}, # No charset!
204+
)
205+
assert await async_setup_component(
206+
hass,
207+
SENSOR_DOMAIN,
208+
{
209+
SENSOR_DOMAIN: {
210+
"name": "mysensor",
211+
"encoding": "iso-8859-1", # This will be used as fallback
212+
"platform": DOMAIN,
213+
"resource": "http://localhost",
214+
"method": "GET",
215+
}
216+
},
217+
)
218+
await hass.async_block_till_done()
219+
assert len(hass.states.async_all(SENSOR_DOMAIN)) == 1
220+
assert hass.states.get("sensor.mysensor").state == "Björk Guðmundsdóttir"
221+
222+
223+
async def test_setup_charset_overrides_encoding_config(
224+
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
225+
) -> None:
226+
"""Test that charset in Content-Type overrides configured encoding."""
227+
# Server sends UTF-8 with correct charset header
228+
aioclient_mock.get(
229+
"http://localhost",
230+
status=HTTPStatus.OK,
231+
content="Björk Guðmundsdóttir".encode(),
232+
headers={"Content-Type": "text/plain; charset=utf-8"},
233+
)
234+
assert await async_setup_component(
235+
hass,
236+
SENSOR_DOMAIN,
237+
{
238+
SENSOR_DOMAIN: {
239+
"name": "mysensor",
240+
"encoding": "iso-8859-1", # Config says ISO-8859-1, but charset=utf-8 should win
241+
"platform": DOMAIN,
242+
"resource": "http://localhost",
243+
"method": "GET",
244+
}
245+
},
246+
)
247+
await hass.async_block_till_done()
248+
assert len(hass.states.async_all(SENSOR_DOMAIN)) == 1
249+
# This should work because charset=utf-8 overrides the iso-8859-1 config
250+
assert hass.states.get("sensor.mysensor").state == "Björk Guðmundsdóttir"
251+
252+
165253
@pytest.mark.parametrize(
166254
("ssl_cipher_list", "ssl_cipher_list_expected"),
167255
[

tests/test_util/aiohttp.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,6 @@ def __init__(
191191
if response is None:
192192
response = b""
193193

194-
self.charset = "utf-8"
195194
self.method = method
196195
self._url = url
197196
self.status = status
@@ -261,16 +260,32 @@ def content(self):
261260
"""Return content."""
262261
return mock_stream(self.response)
263262

263+
@property
264+
def charset(self):
265+
"""Return charset from Content-Type header."""
266+
if (content_type := self._headers.get("content-type")) is None:
267+
return None
268+
content_type = content_type.lower()
269+
if "charset=" in content_type:
270+
return content_type.split("charset=")[1].split(";")[0].strip()
271+
return None
272+
264273
async def read(self):
265274
"""Return mock response."""
266275
return self.response
267276

268-
async def text(self, encoding="utf-8", errors="strict"):
277+
async def text(self, encoding=None, errors="strict") -> str:
269278
"""Return mock response as a string."""
279+
# Match real aiohttp behavior: encoding=None means auto-detect
280+
if encoding is None:
281+
encoding = self.charset or "utf-8"
270282
return self.response.decode(encoding, errors=errors)
271283

272-
async def json(self, encoding="utf-8", content_type=None, loads=json_loads):
284+
async def json(self, encoding=None, content_type=None, loads=json_loads) -> Any:
273285
"""Return mock response as a json."""
286+
# Match real aiohttp behavior: encoding=None means auto-detect
287+
if encoding is None:
288+
encoding = self.charset or "utf-8"
274289
return loads(self.response.decode(encoding))
275290

276291
def release(self):

0 commit comments

Comments
 (0)