fix: replace broad except Exception with specific exception types

- jail_service: catch ValueError (fail2ban protocol error) instead of Exception
- health.py: catch AttributeError (not OSError/TypeError) for defensive checks
- ban_service: re-raise programming errors in geo lookup handlers
- server_service: catch Fail2BanConnectionError, Fail2BanProtocolError, ValueError
- config_writer: catch OSError instead of Exception

Programming errors now bubble to global handler instead of being silently caught.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-03 00:54:44 +02:00
parent bd6170722a
commit 881cfbdd71
5 changed files with 13 additions and 10 deletions

View File

@@ -84,7 +84,7 @@ async def health_check(
components.append( components.append(
ComponentHealth(name="scheduler", healthy=False, message="Not initialised"), ComponentHealth(name="scheduler", healthy=False, message="Not initialised"),
) )
except Exception: # pragma: no cover - defensive except AttributeError: # pragma: no cover - defensive
scheduler_state = "unknown" scheduler_state = "unknown"
components.append( components.append(
ComponentHealth(name="scheduler", healthy=False, message="Not accessible"), ComponentHealth(name="scheduler", healthy=False, message="Not accessible"),
@@ -100,10 +100,11 @@ async def health_check(
components.append( components.append(
ComponentHealth(name="cache", healthy=False, message="Not initialised"), ComponentHealth(name="cache", healthy=False, message="Not initialised"),
) )
except Exception: # pragma: no cover - defensive except AttributeError: # pragma: no cover - defensive
cache_state = "uninitialised" cache_state = "uninitialised"
components.append(
# --- fail2ban --- ComponentHealth(name="cache", healthy=False, message="Not accessible"),
)
fail2ban_online: bool = server_status.online fail2ban_online: bool = server_status.online
if not fail2ban_online: if not fail2ban_online:
components.append( components.append(

View File

@@ -481,6 +481,7 @@ async def list_bans(
log.warning("ban_service_geo_lookup_failed", ip=ip) log.warning("ban_service_geo_lookup_failed", ip=ip)
except Exception as exc: except Exception as exc:
log.error("ban_service_geo_lookup_unexpected_error", ip=ip, error=type(exc).__name__) log.error("ban_service_geo_lookup_unexpected_error", ip=ip, error=type(exc).__name__)
raise # Bubble programming errors to global handler
items.append( items.append(
DomainDashboardBanItem( DomainDashboardBanItem(
@@ -648,7 +649,7 @@ async def bans_by_country(
return ip, None return ip, None
except Exception as exc: except Exception as exc:
log.error("ban_service_geo_lookup_unexpected_error", ip=ip, error=type(exc).__name__) log.error("ban_service_geo_lookup_unexpected_error", ip=ip, error=type(exc).__name__)
return ip, None raise # Bubble programming errors to global handler
results = await asyncio.gather(*(_safe_lookup(ip) for ip in unique_ips)) results = await asyncio.gather(*(_safe_lookup(ip) for ip in unique_ips))
geo_map = {ip: geo for ip, geo in results if geo is not None} geo_map = {ip: geo for ip, geo in results if geo is not None}

View File

@@ -159,12 +159,13 @@ async def _check_backend_cmd_supported(
if state.backend_cmd_supported is not None: if state.backend_cmd_supported is not None:
return state.backend_cmd_supported return state.backend_cmd_supported
# Probe: send the command and catch any exception. # Probe: send the command and catch only fail2ban protocol errors.
# Programming errors (TypeError, AttributeError) bubble up to global handler.
try: try:
ok(await client.send(["get", jail_name, "backend"])) ok(await client.send(["get", jail_name, "backend"]))
state.backend_cmd_supported = True state.backend_cmd_supported = True
log.debug("backend_cmd_supported_detected") log.debug("backend_cmd_supported_detected")
except Exception: except ValueError:
state.backend_cmd_supported = False state.backend_cmd_supported = False
log.debug("backend_cmd_unsupported_detected") log.debug("backend_cmd_unsupported_detected")

View File

@@ -14,7 +14,7 @@ from typing import cast
import structlog import structlog
from app.exceptions import ServerOperationError from app.exceptions import Fail2BanConnectionError, Fail2BanProtocolError, ServerOperationError
from app.models.server_domain import DomainServerSettings, DomainServerSettingsResult from app.models.server_domain import DomainServerSettings, DomainServerSettingsResult
from app.models.server import ServerSettingsUpdate from app.models.server import ServerSettingsUpdate
from app.utils.constants import FAIL2BAN_SOCKET_TIMEOUT from app.utils.constants import FAIL2BAN_SOCKET_TIMEOUT
@@ -79,7 +79,7 @@ async def _safe_get(
try: try:
response = await client.send(command) response = await client.send(command)
return ok(cast("Fail2BanResponse", response)) return ok(cast("Fail2BanResponse", response))
except Exception: except (Fail2BanConnectionError, Fail2BanProtocolError, ValueError):
return default return default

View File

@@ -150,7 +150,7 @@ def _write_parser_atomic(
with os.fdopen(fd, "w", encoding="utf-8") as f: with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(content) f.write(content)
os.replace(tmp_path_str, str(path)) os.replace(tmp_path_str, str(path))
except Exception: except OSError:
with contextlib.suppress(OSError): with contextlib.suppress(OSError):
os.unlink(tmp_path_str) os.unlink(tmp_path_str)
raise raise