Improve exception handling in Pterodactyl (#141955)

Improve exception handling
This commit is contained in:
elmurato 2025-04-03 08:45:06 +02:00 committed by GitHub
parent 03c70e18df
commit 4a562b5085
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 61 additions and 75 deletions

View File

@ -5,20 +5,16 @@ from enum import StrEnum
import logging import logging
from pydactyl import PterodactylClient from pydactyl import PterodactylClient
from pydactyl.exceptions import ( from pydactyl.exceptions import BadRequestError, PterodactylApiError
BadRequestError, from requests.exceptions import ConnectionError, HTTPError
ClientConfigError,
PterodactylApiError,
PydactylError,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class PterodactylConfigurationError(Exception): class PterodactylAuthorizationError(Exception):
"""Raised when the configuration is invalid.""" """Raised when access to server is unauthorized."""
class PterodactylConnectionError(Exception): class PterodactylConnectionError(Exception):
@ -75,13 +71,12 @@ class PterodactylAPI:
paginated_response = await self.hass.async_add_executor_job( paginated_response = await self.hass.async_add_executor_job(
self.pterodactyl.client.servers.list_servers self.pterodactyl.client.servers.list_servers
) )
except ClientConfigError as error: except (BadRequestError, PterodactylApiError, ConnectionError) as error:
raise PterodactylConfigurationError(error) from error raise PterodactylConnectionError(error) from error
except ( except HTTPError as error:
PydactylError, if error.response.status_code == 401:
BadRequestError, raise PterodactylAuthorizationError(error) from error
PterodactylApiError,
) as error:
raise PterodactylConnectionError(error) from error raise PterodactylConnectionError(error) from error
else: else:
game_servers = paginated_response.collect() game_servers = paginated_response.collect()
@ -108,11 +103,12 @@ class PterodactylAPI:
server, utilization = await self.hass.async_add_executor_job( server, utilization = await self.hass.async_add_executor_job(
self.get_server_data, identifier self.get_server_data, identifier
) )
except ( except (BadRequestError, PterodactylApiError, ConnectionError) as error:
PydactylError, raise PterodactylConnectionError(error) from error
BadRequestError, except HTTPError as error:
PterodactylApiError, if error.response.status_code == 401:
) as error: raise PterodactylAuthorizationError(error) from error
raise PterodactylConnectionError(error) from error raise PterodactylConnectionError(error) from error
else: else:
data[identifier] = PterodactylData( data[identifier] = PterodactylData(
@ -145,9 +141,10 @@ class PterodactylAPI:
identifier, identifier,
command, command,
) )
except ( except (BadRequestError, PterodactylApiError, ConnectionError) as error:
PydactylError, raise PterodactylConnectionError(error) from error
BadRequestError, except HTTPError as error:
PterodactylApiError, if error.response.status_code == 401:
) as error: raise PterodactylAuthorizationError(error) from error
raise PterodactylConnectionError(error) from error raise PterodactylConnectionError(error) from error

View File

@ -9,7 +9,11 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .api import PterodactylCommand, PterodactylConnectionError from .api import (
PterodactylAuthorizationError,
PterodactylCommand,
PterodactylConnectionError,
)
from .coordinator import PterodactylConfigEntry, PterodactylCoordinator from .coordinator import PterodactylConfigEntry, PterodactylCoordinator
from .entity import PterodactylEntity from .entity import PterodactylEntity
@ -94,5 +98,9 @@ class PterodactylButtonEntity(PterodactylEntity, ButtonEntity):
) )
except PterodactylConnectionError as err: except PterodactylConnectionError as err:
raise HomeAssistantError( raise HomeAssistantError(
f"Failed to send action '{self.entity_description.key}'" f"Failed to send action '{self.entity_description.key}': Connection error"
) from err
except PterodactylAuthorizationError as err:
raise HomeAssistantError(
f"Failed to send action '{self.entity_description.key}': Unauthorized"
) from err ) from err

View File

@ -13,7 +13,7 @@ from homeassistant.const import CONF_API_KEY, CONF_URL
from .api import ( from .api import (
PterodactylAPI, PterodactylAPI,
PterodactylConfigurationError, PterodactylAuthorizationError,
PterodactylConnectionError, PterodactylConnectionError,
) )
from .const import DOMAIN from .const import DOMAIN
@ -49,7 +49,9 @@ class PterodactylConfigFlow(ConfigFlow, domain=DOMAIN):
try: try:
await api.async_init() await api.async_init()
except (PterodactylConfigurationError, PterodactylConnectionError): except PterodactylAuthorizationError:
errors["base"] = "invalid_auth"
except PterodactylConnectionError:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except Exception: except Exception:
_LOGGER.exception("Unexpected exception occurred during config flow") _LOGGER.exception("Unexpected exception occurred during config flow")

View File

@ -12,7 +12,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .api import ( from .api import (
PterodactylAPI, PterodactylAPI,
PterodactylConfigurationError, PterodactylAuthorizationError,
PterodactylConnectionError, PterodactylConnectionError,
PterodactylData, PterodactylData,
) )
@ -55,12 +55,12 @@ class PterodactylCoordinator(DataUpdateCoordinator[dict[str, PterodactylData]]):
try: try:
await self.api.async_init() await self.api.async_init()
except PterodactylConfigurationError as error: except (PterodactylAuthorizationError, PterodactylConnectionError) as error:
raise UpdateFailed(error) from error raise UpdateFailed(error) from error
async def _async_update_data(self) -> dict[str, PterodactylData]: async def _async_update_data(self) -> dict[str, PterodactylData]:
"""Get updated data from the Pterodactyl server.""" """Get updated data from the Pterodactyl server."""
try: try:
return await self.api.async_get_data() return await self.api.async_get_data()
except PterodactylConnectionError as error: except (PterodactylAuthorizationError, PterodactylConnectionError) as error:
raise UpdateFailed(error) from error raise UpdateFailed(error) from error

View File

@ -14,6 +14,7 @@
}, },
"error": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"
}, },
"abort": { "abort": {

View File

@ -1,8 +1,10 @@
"""Test the Pterodactyl config flow.""" """Test the Pterodactyl config flow."""
from pydactyl import PterodactylClient from pydactyl import PterodactylClient
from pydactyl.exceptions import ClientConfigError, PterodactylApiError from pydactyl.exceptions import BadRequestError, PterodactylApiError
import pytest import pytest
from requests.exceptions import HTTPError
from requests.models import Response
from homeassistant.components.pterodactyl.const import DOMAIN from homeassistant.components.pterodactyl.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER from homeassistant.config_entries import SOURCE_USER
@ -14,6 +16,14 @@ from .conftest import TEST_URL, TEST_USER_INPUT
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
def mock_response():
"""Mock HTTP response."""
mock = Response()
mock.status_code = 401
return mock
@pytest.mark.usefixtures("mock_pterodactyl", "mock_setup_entry") @pytest.mark.usefixtures("mock_pterodactyl", "mock_setup_entry")
async def test_full_flow(hass: HomeAssistant) -> None: async def test_full_flow(hass: HomeAssistant) -> None:
"""Test full flow without errors.""" """Test full flow without errors."""
@ -36,18 +46,21 @@ async def test_full_flow(hass: HomeAssistant) -> None:
@pytest.mark.usefixtures("mock_setup_entry") @pytest.mark.usefixtures("mock_setup_entry")
@pytest.mark.parametrize( @pytest.mark.parametrize(
"exception_type", ("exception_type", "expected_error"),
[ [
ClientConfigError, (PterodactylApiError, "cannot_connect"),
PterodactylApiError, (BadRequestError, "cannot_connect"),
(Exception, "unknown"),
(HTTPError(response=mock_response()), "invalid_auth"),
], ],
) )
async def test_recovery_after_api_error( async def test_recovery_after_error(
hass: HomeAssistant, hass: HomeAssistant,
exception_type, exception_type: Exception,
expected_error: str,
mock_pterodactyl: PterodactylClient, mock_pterodactyl: PterodactylClient,
) -> None: ) -> None:
"""Test recovery after an API error.""" """Test recovery after an error."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
@ -63,42 +76,7 @@ async def test_recovery_after_api_error(
await hass.async_block_till_done() await hass.async_block_till_done()
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "cannot_connect"} assert result["errors"] == {"base": expected_error}
mock_pterodactyl.reset_mock(side_effect=True)
result = await hass.config_entries.flow.async_configure(
flow_id=result["flow_id"], user_input=TEST_USER_INPUT
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == TEST_URL
assert result["data"] == TEST_USER_INPUT
@pytest.mark.usefixtures("mock_setup_entry")
async def test_recovery_after_unknown_error(
hass: HomeAssistant,
mock_pterodactyl: PterodactylClient,
) -> None:
"""Test recovery after an API error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
mock_pterodactyl.client.servers.list_servers.side_effect = Exception
result = await hass.config_entries.flow.async_configure(
flow_id=result["flow_id"],
user_input=TEST_USER_INPUT,
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "unknown"}
mock_pterodactyl.reset_mock(side_effect=True) mock_pterodactyl.reset_mock(side_effect=True)