- Extract jail status/processing to helper functions - Add error_handling.py service for centralized error handling - Update config.py with validation and defaults - Update .env.example with all config options - Remove obsolete Tasks.md, add Service-Development.md - Minor fixes across routers and services Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
3.4 KiB
Service Development Guide
How to write and maintain services in BanGUI.
Error Handling Contracts
Every service method must document which error handling pattern it follows. This lets callers know what to expect without reading the implementation.
The Three Patterns
from app.services.error_handling import ABORT_ON_ERROR, RETURN_DEFAULT, PARTIAL_RESULT
ABORT_ON_ERROR — Raise an exception, let the router convert it to HTTP. Used for: auth, writes, state changes, any operation where partial success is meaningless.
async def start_jail(socket_path: str, name: str) -> None:
"""Start a stopped fail2ban jail.
Error contract: ABORT_ON_ERROR. Raises JailNotFoundError (404),
JailOperationError (409), Fail2BanConnectionError (503).
"""
...
RETURN_DEFAULT — Return empty result and log warning. Never raises. Used for: informational reads (list, get) where infrastructure unavailability should not block the UI.
async def get_settings(socket_path: str) -> DomainServerSettingsResult:
"""Return current fail2ban server-level settings.
Error contract: RETURN_DEFAULT. Returns DomainServerSettingsResult
with default values if socket is unreachable. Never raises.
"""
...
PARTIAL_RESULT — Return (result, errors) tuple. Errors collected, not raised. Used for: batch operations on collections where one item failing does not invalidate the rest.
# Not yet used in codebase; define as needed for batch operations.
When to Use Which
| Operation type | Pattern |
|---|---|
| Auth / session | ABORT_ON_ERROR |
| Write / state change | ABORT_ON_ERROR |
| Config updates | ABORT_ON_ERROR |
| Single-item read (jail, ban) | ABORT_ON_ERROR |
| Multi-item read (list) | RETURN_DEFAULT |
| Server settings read | RETURN_DEFAULT |
| Batch / parallel fetch | PARTIAL_RESULT |
Changing Patterns
Switching a method's error contract is a breaking change. Update the docstring, add a changelog entry, and bump the major version if this is a public API.
Service Structure
Services live in backend/app/services/. They contain no HTTP/FastAPI concerns.
app/services/
ban_service.py # ban/unban, ban history queries
jail_service.py # jail lifecycle, ignore lists
server_service.py # server-level settings
geo_service.py # geolocation
...
error_handling.py # contract definitions
protocols.py # Protocol interfaces for DI
Protocols
Each service has a corresponding protocol in protocols.py for dependency injection.
Protocol methods include the error contract in their docstring:
class JailService(Protocol):
async def list_jails(self, socket_path: str) -> DomainJailList:
"""Error contract: ABORT_ON_ERROR."""
...
Router Error Handling
Routers must not catch and silently swallow exceptions from services using ABORT_ON_ERROR unless they convert to a specific HTTP response. Let domain exceptions propagate — the global exception handlers handle them.
Exception handler registration (in main.py):
DomainError→ JSON error responseFail2BanConnectionError→ HTTP 503JailNotFoundError→ HTTP 404
Logging
Log at the service layer using structlog:
log.info("jail_started", jail=name)
log.warning("socket_unreachable_using_default", socket_path=socket_path)
Never log sensitive data (tokens, passwords, IPs in full).