Refactor services and update documentation

- Refactor ban_service.py with improved error handling
- Refactor blocklist_service.py for better code organization
- Update geo_cache.py with performance improvements
- Update backend development guide and task documentation
- Update runner.csx script

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-26 20:27:04 +02:00
parent 93021500c3
commit bc315b936b
6 changed files with 828 additions and 59 deletions

View File

@@ -611,6 +611,44 @@ async def _fail2ban_connection_handler(request: Request, exc: Fail2BanConnection
)
```
### Service Error Handling Policy
Service methods often call external systems (HTTP APIs, databases, fail2ban) that can fail in diverse ways. To maintain debuggability and predictability:
**Never catch `Exception` broadly** except where unavoidable. Instead, catch specific exception types that match the operation's failure modes:
- **Network I/O**: `TimeoutError`, `aiohttp.ClientError`, `asyncio.TimeoutError`
- **File I/O**: `OSError` (includes `IOError`, `FileNotFoundError`, `PermissionError`)
- **JSON parsing**: `json.JSONDecodeError`, `ValueError`
- **Database errors**: `aiosqlite.Error` and derivatives (caught as `OSError`)
- **Third-party libraries**: Specific exception classes (e.g., `geoip2.errors.GeoIP2Error`)
**When catching service-critical exceptions**:
1. Catch the specific exception types for the operation.
2. Log with the exception type and relevant context.
3. Return a safe fallback (empty dict, None, etc.) or re-raise if the service cannot function.
**When truly unavoidable broad catches are needed** (e.g., retrying transient network failures):
1. Place specific catches first.
2. Add one final `except Exception` **after** specific cases, with `error_type="unexpected"` logged to flag surprises.
3. Document why broad catching is necessary (e.g., "tests use mock objects that may raise arbitrary exceptions").
**Example:**
```python
async def lookup_batch(ips: list[str], http_session: aiohttp.ClientSession) -> dict[str, GeoInfo]:
"""Resolve multiple IPs, returning empty map on failure."""
try:
result = await http_session.post(url, json=payload, timeout=timeout)
except (TimeoutError, aiohttp.ClientError) as exc:
# Expected network failures — log and return empty result
log.warning("geo_batch_http_failed", error=type(exc).__name__)
return {}
except Exception as exc:
# Unexpected — log as error for investigation
log.error("geo_batch_unexpected_error", error=type(exc).__name__)
return {}
```
---
## 9. Testing