Implement centralized exception handling and validation
- Add custom exception classes for structured error handling - Implement global exception handlers in FastAPI application - Add comprehensive request/response validation - Create exception contract tests for validation - Update backend development documentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -690,11 +690,90 @@ async def _fail2ban_connection_handler(request: Request, exc: Fail2BanConnection
|
||||
)
|
||||
```
|
||||
|
||||
### Service Error Handling Policy
|
||||
### Exception Taxonomy and HTTP Mapping
|
||||
|
||||
BanGUI uses a **standardized exception taxonomy** that maps domain errors to HTTP status codes consistently across all services. This allows routers to handle exceptions by category rather than by individual type, reducing code duplication and ensuring consistent client-facing error responses.
|
||||
|
||||
#### Exception Categories
|
||||
|
||||
All domain exceptions inherit from one of six base categories defined in `app.exceptions`:
|
||||
|
||||
| Base Exception | HTTP Status | Meaning | Example |
|
||||
|---|---|---|---|
|
||||
| `NotFoundError` | 404 | Requested domain entity not found | `JailNotFoundError`, `FilterNotFoundError` |
|
||||
| `BadRequestError` | 400 | Invalid input, validation failure, or invalid identifier | `ConfigValidationError`, `JailNameError` |
|
||||
| `ConflictError` | 409 | State conflict or resource constraint violation | `JailAlreadyActiveError`, `FilterAlreadyExistsError` |
|
||||
| `OperationError` | 500 | Domain operation failure (write, update, delete) | `ConfigWriteError`, `ConfigFileWriteError` |
|
||||
| `ServiceUnavailableError` | 503 | Infrastructure or external service unreachable | `Fail2BanConnectionError`, `ConfigDirError` |
|
||||
|
||||
#### Service Exception Mapping
|
||||
|
||||
Every service-specific exception inherits from exactly one category. This allows `main.py` to register just **5 exception handlers** instead of 25+:
|
||||
|
||||
```python
|
||||
# In app/exceptions.py — define each exception once with its category
|
||||
class JailNotFoundError(NotFoundError):
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
super().__init__(f"Jail not found: {name!r}")
|
||||
|
||||
class JailAlreadyActiveError(ConflictError):
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
super().__init__(f"Jail is already active: {name!r}")
|
||||
|
||||
# In app/main.py — register category handlers
|
||||
app.add_exception_handler(NotFoundError, _not_found_handler)
|
||||
app.add_exception_handler(BadRequestError, _bad_request_handler)
|
||||
app.add_exception_handler(ConflictError, _conflict_handler)
|
||||
app.add_exception_handler(OperationError, _domain_error_handler)
|
||||
app.add_exception_handler(ServiceUnavailableError, _service_unavailable_handler)
|
||||
```
|
||||
|
||||
#### Service Exception Reference
|
||||
|
||||
When writing a new service, determine which category each exception belongs to:
|
||||
|
||||
- **Not found**: Always `NotFoundError` (e.g., jail, filter, action, config file not found)
|
||||
- **Invalid input**: Always `BadRequestError` (e.g., validation errors, invalid names, regex compile failure)
|
||||
- **State conflicts**: Always `ConflictError` (e.g., already exists, already active, readonly resource)
|
||||
- **Operation failures**: Always `OperationError` (e.g., write failed, update failed, command failed)
|
||||
- **Infrastructure**: Always `ServiceUnavailableError` (e.g., config dir missing, socket unreachable, fail2ban protocol error)
|
||||
|
||||
#### Client Expectations
|
||||
|
||||
Clients should expect the following HTTP status codes and response format for all domain errors:
|
||||
|
||||
```json
|
||||
HTTP 400 Bad Request
|
||||
{
|
||||
"detail": "Jail name contains invalid characters"
|
||||
}
|
||||
|
||||
HTTP 404 Not Found
|
||||
{
|
||||
"detail": "Jail not found: 'sshd'"
|
||||
}
|
||||
|
||||
HTTP 409 Conflict
|
||||
{
|
||||
"detail": "Jail is already active: 'sshd'"
|
||||
}
|
||||
|
||||
HTTP 500 Internal Server Error
|
||||
{
|
||||
"detail": "Failed to write configuration: permission denied"
|
||||
}
|
||||
|
||||
HTTP 503 Service Unavailable
|
||||
{
|
||||
"detail": "Cannot reach the fail2ban service. Check the server status page."
|
||||
}
|
||||
```
|
||||
|
||||
The `detail` field always contains the exception's message (from `str(exc)`). Sensitive details (socket paths, file paths, internal error messages) are never included — they are logged server-side only.
|
||||
|
||||
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`)
|
||||
|
||||
@@ -1,22 +1,3 @@
|
||||
## 4) Module-level mutable runtime flags in service layer
|
||||
- Where found:
|
||||
- [backend/app/services/jail_service.py](backend/app/services/jail_service.py)
|
||||
- Why this is needed:
|
||||
- Global mutable state is difficult to reason about under concurrency and tests.
|
||||
- Goal:
|
||||
- Move mutable runtime state to managed app/runtime state services.
|
||||
- What to do:
|
||||
- Replace module-level flags with injected state holder.
|
||||
- Guard mutations with clear synchronization boundaries.
|
||||
- Possible traps and issues:
|
||||
- Race conditions can reappear if state updates are spread across modules.
|
||||
- Docs changes needed:
|
||||
- Document allowed mutable-state locations.
|
||||
- Doc references:
|
||||
- [Docs/Architekture.md](Docs/Architekture.md)
|
||||
|
||||
---
|
||||
|
||||
## 5) Inconsistent domain exception contracts across services
|
||||
- Where found:
|
||||
- [backend/app/routers/jails.py](backend/app/routers/jails.py)
|
||||
|
||||
Reference in New Issue
Block a user