# 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 ```python 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. ```python 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. ```python 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. ```python # 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: ```python 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 response - `Fail2BanConnectionError` → HTTP 503 - `JailNotFoundError` → HTTP 404 ## Logging Log at the service layer using structlog: ```python 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).