Error Handling and Exception Management in Shadowbroker's FastAPI Backend
Shadowbroker’s backend implements a three-layer error-handling strategy that uses domain-specific custom exceptions, converts them to FastAPI HTTPException for clients, and enforces strict logging rules to ensure every failure is observable.
The BigBodyCobain/Shadowbroker repository contains a Python backend built on FastAPI that treats error handling and exception management as a first-class concern. Rather than relying on generic try/except blocks, the codebase structures failure handling across service, API, and middleware layers to maintain both operational observability and consistent client error responses.
Domain-Specific Custom Exceptions
The service layer in Shadowbroker defines explicit exception types to represent distinct failure modes. In backend/services/unusual_whales_connector.py, the code declares classes like FinnhubConnectorError, ShodanConnectorError, and PrivacyCoreError for domain-specific failures.
These custom exceptions wrap low-level HTTP errors from external APIs, allowing the service layer to propagate meaningful error context upward without leaking implementation details. When a connector encounters an httpx.HTTPError, it raises a specific domain exception that includes the original cause, preserving the stack trace while translating the error into backend-internal terminology.
FastAPI HTTPException for Client-Facing Errors
The API layer in backend/wormhole_server.py bridges service-level exceptions to HTTP responses. When a domain exception reaches an endpoint, the code converts it to a fastapi.HTTPException with an appropriate status code.
# backend/wormhole_server.py
from fastapi import APIRouter, HTTPException
from .services.unusual_whales_connector import (
FinnhubConnectorError,
fetch_financial_data,
)
router = APIRouter()
@router.get("/api/finance/{symbol}")
async def get_finance(symbol: str):
try:
data = fetch_financial_data(symbol)
except FinnhubConnectorError as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
return {"symbol": symbol, "quote": data}
This pattern ensures that clients receive structured HTTP error codes—such as 502 Bad Gateway for third-party failures—while the backend maintains internal exception hierarchies.
Centralized Logging and Exception Visibility
Every exception handler in the Shadowbroker backend must capture the exception object as exc. The codebase strictly prohibits bare except: clauses. In backend/services/wormhole_supervisor.py, unexpected exceptions are logged with full stack traces before conversion to HTTP responses.
# backend/services/wormhole_supervisor.py
import logging
from fastapi import HTTPException
log = logging.getLogger("wormhole_supervisor")
def run_supervisor():
try:
# Core supervisory logic ...
pass
except Exception as exc:
log.error("Supervisor failure", exc_info=exc)
raise HTTPException(status_code=500, detail="Internal supervisor error") from exc
Log messages emit at DEBUG level for silent failures that are re-raised, and ERROR level for hard failures that terminate requests. This guarantees that no exception passes through the system without generating observability data.
Graceful Degradation for Optional Features
The backend handles optional enrichment services using defensive try/except blocks that log failures but allow the main request to continue. In backend/services/region_dossier.py, optional lookups wrap their calls to prevent a single service outage from breaking the entire response.
This approach isolates non-critical dependencies, ensuring that auxiliary data sources—such as open-source intelligence fetches—fail independently without cascading to the primary request flow.
Test-Driven Exception Hygiene
The repository enforces exception handling standards through automated testing. The file backend/tests/test_2c_exception_visibility.py scans the source tree to verify two rules:
- Every
except Exceptionclause must capture the exception object (except Exception as exc:) - No bare
except Exception: passstatements exist
This test-driven approach prevents "silent" exception swallowing that could hide production bugs.
Uniform JSON Response Format
FastAPI global exception handlers in backend/app.py ensure all errors—handled and unhandled—return a consistent JSON structure. The response includes detail, code, and type fields, making client-side error handling predictable.
# backend/app.py
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
import logging
app = FastAPI()
log = logging.getLogger("shadowbroker")
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
log.debug("HTTPException from %s: %s", request.url.path, exc.detail)
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail, "code": exc.status_code, "type": "http_error"},
)
@app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception):
log.error("Unhandled exception on %s", request.url.path, exc_info=exc)
return JSONResponse(
status_code=500,
content={
"detail": "An unexpected error occurred",
"code": 500,
"type": "internal_error",
},
)
Code Examples
Defining a Domain-Specific Exception
The following pattern from backend/services/unusual_whales_connector.py demonstrates wrapping third-party HTTP errors in custom exceptions:
# backend/services/unusual_whales_connector.py
class FinnhubConnectorError(Exception):
"""Raised when the Finnhub API returns an unexpected response."""
pass
def fetch_financial_data(symbol: str) -> dict:
try:
resp = httpx.get(f"https://finnhub.io/api/v1/quote?symbol={symbol}")
resp.raise_for_status()
except httpx.HTTPError as e:
raise FinnhubConnectorError(f"Finnhub request failed: {e}") from e
return resp.json()
Converting Service Errors to HTTP Responses
This snippet from backend/wormhole_server.py shows the translation layer between domain exceptions and client-facing HTTP errors:
# backend/wormhole_server.py
@router.get("/api/finance/{symbol}")
async def get_finance(symbol: str):
try:
data = fetch_financial_data(symbol)
except FinnhubConnectorError as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
return {"symbol": symbol, "quote": data}
Summary
- Domain-specific exceptions like
FinnhubConnectorErrorisolate service-layer failures from transport-layer details - FastAPI HTTPException converts internal errors to standardized HTTP responses with appropriate status codes
- Mandatory exception capture (
except Exception as exc) and structured logging ensure complete observability - Graceful degradation in optional services prevents cascading failures from non-critical components
- Automated testing enforces that no exceptions are silently swallowed via bare except clauses
Frequently Asked Questions
What custom exceptions does Shadowbroker define?
Shadowbroker defines domain-specific exceptions in backend/services/unusual_whales_connector.py, including FinnhubConnectorError, ShodanConnectorError, and PrivacyCoreError. These represent failure modes specific to external data connectors rather than generic Python built-ins.
How does Shadowbroker prevent silent exception swallowing?
The repository includes backend/tests/test_2c_exception_visibility.py, which statically analyzes the codebase to enforce that every exception handler captures the exception object as exc. This prevents bare except Exception: pass patterns that could hide runtime errors from logs and monitoring systems.
Why does Shadowbroker use FastAPI HTTPException instead of returning error responses directly?
Using fastapi.HTTPException allows the framework to handle JSON serialization, status code assignment, and header management automatically. It also ensures that all error responses flow through the same middleware pipeline, maintaining uniform response formatting across the API surface.
How does Shadowbroker handle failures in optional enrichment services?
Optional features like region dossier lookups in backend/services/region_dossier.py wrap external calls in try/except blocks that log the failure at debug level but continue execution. This pattern provides graceful degradation, ensuring that auxiliary data sources failing do not break the primary request flow.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →