Set timeout for remote calendar (#147024)

This commit is contained in:
Thomas55555 2025-07-03 19:08:58 +02:00 committed by GitHub
parent 01b4a5ceed
commit 4a937d2452
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 48 additions and 16 deletions

View File

@ -0,0 +1,12 @@
"""Specifies the parameter for the httpx download."""
from httpx import AsyncClient, Response, Timeout
async def get_calendar(client: AsyncClient, url: str) -> Response:
"""Make an HTTP GET request using Home Assistant's async HTTPX client with timeout."""
return await client.get(
url,
follow_redirects=True,
timeout=Timeout(5, read=30, write=5, pool=5),
)

View File

@ -4,13 +4,14 @@ from http import HTTPStatus
import logging
from typing import Any
from httpx import HTTPError, InvalidURL
from httpx import HTTPError, InvalidURL, TimeoutException
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_URL
from homeassistant.helpers.httpx_client import get_async_client
from .client import get_calendar
from .const import CONF_CALENDAR_NAME, DOMAIN
from .ics import InvalidIcsException, parse_calendar
@ -49,7 +50,7 @@ class RemoteCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]})
client = get_async_client(self.hass)
try:
res = await client.get(user_input[CONF_URL], follow_redirects=True)
res = await get_calendar(client, user_input[CONF_URL])
if res.status_code == HTTPStatus.FORBIDDEN:
errors["base"] = "forbidden"
return self.async_show_form(
@ -58,9 +59,14 @@ class RemoteCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
res.raise_for_status()
except TimeoutException as err:
errors["base"] = "timeout_connect"
_LOGGER.debug(
"A timeout error occurred: %s", str(err) or type(err).__name__
)
except (HTTPError, InvalidURL) as err:
errors["base"] = "cannot_connect"
_LOGGER.debug("An error occurred: %s", err)
_LOGGER.debug("An error occurred: %s", str(err) or type(err).__name__)
else:
try:
await parse_calendar(self.hass, res.text)

View File

@ -3,7 +3,7 @@
from datetime import timedelta
import logging
from httpx import HTTPError, InvalidURL
from httpx import HTTPError, InvalidURL, TimeoutException
from ical.calendar import Calendar
from homeassistant.config_entries import ConfigEntry
@ -12,6 +12,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .client import get_calendar
from .const import DOMAIN
from .ics import InvalidIcsException, parse_calendar
@ -36,7 +37,7 @@ class RemoteCalendarDataUpdateCoordinator(DataUpdateCoordinator[Calendar]):
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
name=f"{DOMAIN}_{config_entry.title}",
update_interval=SCAN_INTERVAL,
always_update=True,
)
@ -46,13 +47,19 @@ class RemoteCalendarDataUpdateCoordinator(DataUpdateCoordinator[Calendar]):
async def _async_update_data(self) -> Calendar:
"""Update data from the url."""
try:
res = await self._client.get(self._url, follow_redirects=True)
res = await get_calendar(self._client, self._url)
res.raise_for_status()
except TimeoutException as err:
_LOGGER.debug("%s: %s", self._url, str(err) or type(err).__name__)
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="timeout",
) from err
except (HTTPError, InvalidURL) as err:
_LOGGER.debug("%s: %s", self._url, str(err) or type(err).__name__)
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="unable_to_fetch",
translation_placeholders={"err": str(err)},
) from err
try:
self.ics = res.text

View File

@ -18,14 +18,18 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
},
"error": {
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"forbidden": "The server understood the request but refuses to authorize it.",
"invalid_ics_file": "There was a problem reading the calendar information. See the error log for additional details."
}
},
"exceptions": {
"timeout": {
"message": "The connection timed out. See the debug log for additional details."
},
"unable_to_fetch": {
"message": "Unable to fetch calendar data: {err}"
"message": "Unable to fetch calendar data. See the debug log for additional details."
},
"unable_to_parse": {
"message": "Unable to parse calendar data: {err}"

View File

@ -1,6 +1,6 @@
"""Test the Remote Calendar config flow."""
from httpx import ConnectError, Response, UnsupportedProtocol
from httpx import HTTPError, InvalidURL, Response, TimeoutException
import pytest
import respx
@ -75,10 +75,11 @@ async def test_form_import_webcal(hass: HomeAssistant, ics_content: str) -> None
@pytest.mark.parametrize(
("side_effect"),
("side_effect", "base_error"),
[
ConnectError("Connection failed"),
UnsupportedProtocol("Unsupported protocol"),
(TimeoutException("Connection timed out"), "timeout_connect"),
(HTTPError("Connection failed"), "cannot_connect"),
(InvalidURL("Unsupported protocol"), "cannot_connect"),
],
)
@respx.mock
@ -86,6 +87,7 @@ async def test_form_inavild_url(
hass: HomeAssistant,
side_effect: Exception,
ics_content: str,
base_error: str,
) -> None:
"""Test we get the import form."""
result = await hass.config_entries.flow.async_init(
@ -102,7 +104,7 @@ async def test_form_inavild_url(
},
)
assert result2["type"] is FlowResultType.FORM
assert result2["errors"] == {"base": "cannot_connect"}
assert result2["errors"] == {"base": base_error}
respx.get(CALENDER_URL).mock(
return_value=Response(
status_code=200,

View File

@ -1,6 +1,6 @@
"""Tests for init platform of Remote Calendar."""
from httpx import ConnectError, Response, UnsupportedProtocol
from httpx import HTTPError, InvalidURL, Response, TimeoutException
import pytest
import respx
@ -56,8 +56,9 @@ async def test_raise_for_status(
@pytest.mark.parametrize(
"side_effect",
[
ConnectError("Connection failed"),
UnsupportedProtocol("Unsupported protocol"),
TimeoutException("Connection timed out"),
HTTPError("Connection failed"),
InvalidURL("Unsupported protocol"),
ValueError("Invalid response"),
],
)