refactor: Remove duplicate router-level exception helpers

All routers now let domain exceptions propagate to the global handlers in main.py
instead of catching and converting them to HTTPException. This eliminates:

- Duplicate exception-to-HTTP-status mappings across 8 routers
- Duplicate helper functions (_bad_gateway, _not_found, _conflict, etc.)
- Inconsistent error response formats

Changes:
- Removed all try/except blocks from routers that catch domain exceptions
- Removed duplicate helper functions from all routers
- Added missing exception handlers to main.py for:
  * ActionNameError
  * FilterNameError
  * JailNameError
  * JailNotFoundInConfigError
  * FilterInvalidRegexError
- Removed unused imports from affected routers

All domain exceptions now propagate to the single authoritative mapping in
main.py, ensuring consistent error codes, messages, and logging across the API.

Affected routers:
- action_config.py: Removed _action_not_found, _bad_request, _not_found helpers
- bans.py: Removed try/except in ban/unban endpoints
- config_misc.py: Removed try/except blocks
- file_config.py: Removed 6 try/except blocks and _service_unavailable helper
- filter_config.py: Removed try/except blocks
- geo.py: Removed try/except in lookup_ip endpoint
- jail_config.py: Removed try/except blocks
- jails.py: Removed try/except blocks
- server.py: Removed try/except blocks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-23 16:00:37 +02:00
parent b634ce876a
commit 5480dce221
12 changed files with 229 additions and 977 deletions

View File

@@ -31,7 +31,7 @@ from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, HTTPException, Path, status
from fastapi import APIRouter, Path, status
from app.dependencies import AuthDep, Fail2BanConfigDirDep
from app.models.config import (
@@ -52,13 +52,6 @@ from app.models.file_config import (
JailConfigFilesResponse,
)
from app.services import raw_config_io_service
from app.exceptions import (
ConfigDirError,
ConfigFileExistsError,
ConfigFileNameError,
ConfigFileNotFoundError,
ConfigFileWriteError,
)
router: APIRouter = APIRouter(prefix="/api/config", tags=["Config"])
@@ -73,39 +66,6 @@ _NamePath = Annotated[
str, Path(description="Base name with or without extension (e.g. ``sshd`` or ``sshd.conf``).")
]
# ---------------------------------------------------------------------------
# Error helpers
# ---------------------------------------------------------------------------
def _not_found(filename: str) -> HTTPException:
return HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Config file not found: {filename!r}",
)
def _bad_request(message: str) -> HTTPException:
return HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=message,
)
def _conflict(filename: str) -> HTTPException:
return HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Config file already exists: {filename!r}",
)
def _service_unavailable(message: str) -> HTTPException:
return HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=message,
)
# ---------------------------------------------------------------------------
# Jail config file endpoints (Task 4a)
# ---------------------------------------------------------------------------
@@ -132,12 +92,7 @@ async def list_jail_config_files(
Returns:
:class:`~app.models.file_config.JailConfigFilesResponse`.
"""
try:
return await raw_config_io_service.list_jail_config_files(config_dir)
except ConfigDirError as exc:
raise _service_unavailable(str(exc)) from exc
return await raw_config_io_service.list_jail_config_files(config_dir)
@router.get(
"/jail-files/{filename}",
response_model=JailConfigFileContent,
@@ -163,16 +118,7 @@ async def get_jail_config_file(
HTTPException: 404 if the file does not exist.
HTTPException: 503 if the config directory is unavailable.
"""
try:
return await raw_config_io_service.get_jail_config_file(config_dir, filename)
except ConfigFileNameError as exc:
raise _bad_request(str(exc)) from exc
except ConfigFileNotFoundError:
raise _not_found(filename) from None
except ConfigDirError as exc:
raise _service_unavailable(str(exc)) from exc
return await raw_config_io_service.get_jail_config_file(config_dir, filename)
@router.put(
"/jail-files/{filename}",
status_code=status.HTTP_204_NO_CONTENT,
@@ -200,18 +146,7 @@ async def write_jail_config_file(
HTTPException: 404 if the file does not exist.
HTTPException: 503 if the config directory is unavailable.
"""
try:
await raw_config_io_service.write_jail_config_file(config_dir, filename, body)
except ConfigFileNameError as exc:
raise _bad_request(str(exc)) from exc
except ConfigFileNotFoundError:
raise _not_found(filename) from None
except ConfigFileWriteError as exc:
raise _bad_request(str(exc)) from exc
except ConfigDirError as exc:
raise _service_unavailable(str(exc)) from exc
await raw_config_io_service.write_jail_config_file(config_dir, filename, body)
@router.put(
"/jail-files/{filename}/enabled",
status_code=status.HTTP_204_NO_CONTENT,
@@ -239,20 +174,9 @@ async def set_jail_config_file_enabled(
HTTPException: 404 if the file does not exist.
HTTPException: 503 if the config directory is unavailable.
"""
try:
await raw_config_io_service.set_jail_config_enabled(
config_dir, filename, body.enabled
)
except ConfigFileNameError as exc:
raise _bad_request(str(exc)) from exc
except ConfigFileNotFoundError:
raise _not_found(filename) from None
except ConfigFileWriteError as exc:
raise _bad_request(str(exc)) from exc
except ConfigDirError as exc:
raise _service_unavailable(str(exc)) from exc
await raw_config_io_service.set_jail_config_enabled(
config_dir, filename, body.enabled
)
@router.post(
"/jail-files",
response_model=ConfFileContent,
@@ -279,17 +203,7 @@ async def create_jail_config_file(
HTTPException: 409 if a file with that name already exists.
HTTPException: 503 if the config directory is unavailable.
"""
try:
filename = await raw_config_io_service.create_jail_config_file(config_dir, body)
except ConfigFileNameError as exc:
raise _bad_request(str(exc)) from exc
except ConfigFileExistsError:
raise _conflict(body.name) from None
except ConfigFileWriteError as exc:
raise _bad_request(str(exc)) from exc
except ConfigDirError as exc:
raise _service_unavailable(str(exc)) from exc
filename = await raw_config_io_service.create_jail_config_file(config_dir, body)
return ConfFileContent(
name=body.name,
filename=filename,
@@ -331,16 +245,7 @@ async def get_filter_file_raw(
HTTPException: 404 if the file does not exist.
HTTPException: 503 if the config directory is unavailable.
"""
try:
return await raw_config_io_service.get_filter_file(config_dir, name)
except ConfigFileNameError as exc:
raise _bad_request(str(exc)) from exc
except ConfigFileNotFoundError:
raise _not_found(name) from None
except ConfigDirError as exc:
raise _service_unavailable(str(exc)) from exc
return await raw_config_io_service.get_filter_file(config_dir, name)
@router.put(
"/filters/{name}/raw",
status_code=status.HTTP_204_NO_CONTENT,
@@ -365,18 +270,7 @@ async def write_filter_file(
HTTPException: 404 if the file does not exist.
HTTPException: 503 if the config directory is unavailable.
"""
try:
await raw_config_io_service.write_filter_file(config_dir, name, body)
except ConfigFileNameError as exc:
raise _bad_request(str(exc)) from exc
except ConfigFileNotFoundError:
raise _not_found(name) from None
except ConfigFileWriteError as exc:
raise _bad_request(str(exc)) from exc
except ConfigDirError as exc:
raise _service_unavailable(str(exc)) from exc
await raw_config_io_service.write_filter_file(config_dir, name, body)
@router.post(
"/filters/raw",
status_code=status.HTTP_201_CREATED,
@@ -403,17 +297,7 @@ async def create_filter_file(
HTTPException: 409 if a file with that name already exists.
HTTPException: 503 if the config directory is unavailable.
"""
try:
filename = await raw_config_io_service.create_filter_file(config_dir, body)
except ConfigFileNameError as exc:
raise _bad_request(str(exc)) from exc
except ConfigFileExistsError:
raise _conflict(body.name) from None
except ConfigFileWriteError as exc:
raise _bad_request(str(exc)) from exc
except ConfigDirError as exc:
raise _service_unavailable(str(exc)) from exc
filename = await raw_config_io_service.create_filter_file(config_dir, body)
return ConfFileContent(
name=body.name,
filename=filename,
@@ -444,12 +328,7 @@ async def list_action_files(
Returns:
:class:`~app.models.file_config.ConfFilesResponse`.
"""
try:
return await raw_config_io_service.list_action_files(config_dir)
except ConfigDirError as exc:
raise _service_unavailable(str(exc)) from exc
return await raw_config_io_service.list_action_files(config_dir)
@router.get(
"/actions/{name}/raw",
response_model=ConfFileContent,
@@ -475,16 +354,7 @@ async def get_action_file(
HTTPException: 404 if the file does not exist.
HTTPException: 503 if the config directory is unavailable.
"""
try:
return await raw_config_io_service.get_action_file(config_dir, name)
except ConfigFileNameError as exc:
raise _bad_request(str(exc)) from exc
except ConfigFileNotFoundError:
raise _not_found(name) from None
except ConfigDirError as exc:
raise _service_unavailable(str(exc)) from exc
return await raw_config_io_service.get_action_file(config_dir, name)
@router.put(
"/actions/{name}/raw",
status_code=status.HTTP_204_NO_CONTENT,
@@ -509,18 +379,7 @@ async def write_action_file(
HTTPException: 404 if the file does not exist.
HTTPException: 503 if the config directory is unavailable.
"""
try:
await raw_config_io_service.write_action_file(config_dir, name, body)
except ConfigFileNameError as exc:
raise _bad_request(str(exc)) from exc
except ConfigFileNotFoundError:
raise _not_found(name) from None
except ConfigFileWriteError as exc:
raise _bad_request(str(exc)) from exc
except ConfigDirError as exc:
raise _service_unavailable(str(exc)) from exc
await raw_config_io_service.write_action_file(config_dir, name, body)
@router.post(
"/actions",
status_code=status.HTTP_201_CREATED,
@@ -547,17 +406,7 @@ async def create_action_file(
HTTPException: 409 if a file with that name already exists.
HTTPException: 503 if the config directory is unavailable.
"""
try:
filename = await raw_config_io_service.create_action_file(config_dir, body)
except ConfigFileNameError as exc:
raise _bad_request(str(exc)) from exc
except ConfigFileExistsError:
raise _conflict(body.name) from None
except ConfigFileWriteError as exc:
raise _bad_request(str(exc)) from exc
except ConfigDirError as exc:
raise _service_unavailable(str(exc)) from exc
filename = await raw_config_io_service.create_action_file(config_dir, body)
return ConfFileContent(
name=body.name,
filename=filename,
@@ -599,16 +448,7 @@ async def get_parsed_filter(
HTTPException: 404 if the file does not exist.
HTTPException: 503 if the config directory is unavailable.
"""
try:
return await raw_config_io_service.get_parsed_filter_file(config_dir, name)
except ConfigFileNameError as exc:
raise _bad_request(str(exc)) from exc
except ConfigFileNotFoundError:
raise _not_found(name) from None
except ConfigDirError as exc:
raise _service_unavailable(str(exc)) from exc
return await raw_config_io_service.get_parsed_filter_file(config_dir, name)
@router.put(
"/filters/{name}/parsed",
status_code=status.HTTP_204_NO_CONTENT,
@@ -636,18 +476,7 @@ async def update_parsed_filter(
HTTPException: 404 if the file does not exist.
HTTPException: 503 if the config directory is unavailable.
"""
try:
await raw_config_io_service.update_parsed_filter_file(config_dir, name, body)
except ConfigFileNameError as exc:
raise _bad_request(str(exc)) from exc
except ConfigFileNotFoundError:
raise _not_found(name) from None
except ConfigFileWriteError as exc:
raise _bad_request(str(exc)) from exc
except ConfigDirError as exc:
raise _service_unavailable(str(exc)) from exc
await raw_config_io_service.update_parsed_filter_file(config_dir, name, body)
# ---------------------------------------------------------------------------
# Parsed action endpoints (Task 3.1)
# ---------------------------------------------------------------------------
@@ -682,16 +511,7 @@ async def get_parsed_action(
HTTPException: 404 if the file does not exist.
HTTPException: 503 if the config directory is unavailable.
"""
try:
return await raw_config_io_service.get_parsed_action_file(config_dir, name)
except ConfigFileNameError as exc:
raise _bad_request(str(exc)) from exc
except ConfigFileNotFoundError:
raise _not_found(name) from None
except ConfigDirError as exc:
raise _service_unavailable(str(exc)) from exc
return await raw_config_io_service.get_parsed_action_file(config_dir, name)
@router.put(
"/actions/{name}/parsed",
status_code=status.HTTP_204_NO_CONTENT,
@@ -719,18 +539,7 @@ async def update_parsed_action(
HTTPException: 404 if the file does not exist.
HTTPException: 503 if the config directory is unavailable.
"""
try:
await raw_config_io_service.update_parsed_action_file(config_dir, name, body)
except ConfigFileNameError as exc:
raise _bad_request(str(exc)) from exc
except ConfigFileNotFoundError:
raise _not_found(name) from None
except ConfigFileWriteError as exc:
raise _bad_request(str(exc)) from exc
except ConfigDirError as exc:
raise _service_unavailable(str(exc)) from exc
await raw_config_io_service.update_parsed_action_file(config_dir, name, body)
# ---------------------------------------------------------------------------
# Parsed jail file endpoints (Task 6.1)
# ---------------------------------------------------------------------------
@@ -765,16 +574,7 @@ async def get_parsed_jail_file(
HTTPException: 404 if the file does not exist.
HTTPException: 503 if the config directory is unavailable.
"""
try:
return await raw_config_io_service.get_parsed_jail_file(config_dir, filename)
except ConfigFileNameError as exc:
raise _bad_request(str(exc)) from exc
except ConfigFileNotFoundError:
raise _not_found(filename) from None
except ConfigDirError as exc:
raise _service_unavailable(str(exc)) from exc
return await raw_config_io_service.get_parsed_jail_file(config_dir, filename)
@router.put(
"/jail-files/{filename}/parsed",
status_code=status.HTTP_204_NO_CONTENT,
@@ -802,13 +602,4 @@ async def update_parsed_jail_file(
HTTPException: 404 if the file does not exist.
HTTPException: 503 if the config directory is unavailable.
"""
try:
await raw_config_io_service.update_parsed_jail_file(config_dir, filename, body)
except ConfigFileNameError as exc:
raise _bad_request(str(exc)) from exc
except ConfigFileNotFoundError:
raise _not_found(filename) from None
except ConfigFileWriteError as exc:
raise _bad_request(str(exc)) from exc
except ConfigDirError as exc:
raise _service_unavailable(str(exc)) from exc
await raw_config_io_service.update_parsed_jail_file(config_dir, filename, body)