## 27) Error response body shape is inconsistent
This commit is contained in:
@@ -22,7 +22,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, HTTPException, Request, Response, status
|
||||
from fastapi import APIRouter, Request, Response, status
|
||||
|
||||
from app.dependencies import (
|
||||
AuthDep,
|
||||
@@ -31,6 +31,7 @@ from app.dependencies import (
|
||||
SessionServiceContextDep,
|
||||
SettingsDep,
|
||||
)
|
||||
from app.exceptions import AuthenticationError, RateLimitError
|
||||
from app.models.auth import LoginRequest, LoginResponse, LogoutResponse
|
||||
from app.services import auth_service
|
||||
from app.utils.client_ip import get_client_ip
|
||||
@@ -79,18 +80,14 @@ async def login(
|
||||
:class:`~app.models.auth.LoginResponse` containing the token.
|
||||
|
||||
Raises:
|
||||
HTTPException: 401 if the password is incorrect.
|
||||
HTTPException: 429 if the rate limit is exceeded.
|
||||
AuthenticationError: if the password is incorrect.
|
||||
RateLimitError: if the rate limit is exceeded.
|
||||
"""
|
||||
client_ip = get_client_ip(request, trusted_proxies=_TRUSTED_PROXIES)
|
||||
|
||||
if not rate_limiter.is_allowed(client_ip):
|
||||
log.warning("login_rate_limit_exceeded", client_ip=client_ip)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
||||
detail="Too many login attempts. Please try again later.",
|
||||
headers={"Retry-After": "60"},
|
||||
)
|
||||
raise RateLimitError("Too many login attempts. Please try again later.")
|
||||
|
||||
try:
|
||||
signed_token, expires_at = await auth_service.login(
|
||||
@@ -106,10 +103,7 @@ async def login(
|
||||
# but an extra 10 seconds makes automation much less feasible.
|
||||
await asyncio.sleep(10.0)
|
||||
log.warning("login_failed", client_ip=client_ip, error=str(exc))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
raise AuthenticationError(str(exc)) from exc
|
||||
|
||||
response.set_cookie(
|
||||
key=SESSION_COOKIE_NAME,
|
||||
|
||||
@@ -22,7 +22,7 @@ registered *before* the ``/{id}`` routes so FastAPI resolves them correctly.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, status
|
||||
from fastapi import APIRouter, Query, status
|
||||
|
||||
from app.dependencies import (
|
||||
AuthDep,
|
||||
@@ -33,6 +33,7 @@ from app.dependencies import (
|
||||
SchedulerDep,
|
||||
SettingsDep,
|
||||
)
|
||||
from app.exceptions import BadRequestError, BlocklistSourceNotFoundError
|
||||
from app.models.blocklist import (
|
||||
BlocklistListResponse,
|
||||
BlocklistSource,
|
||||
@@ -107,7 +108,7 @@ async def create_blocklist(
|
||||
blocklist_ctx.db, payload.name, str(payload.url), enabled=payload.enabled
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
raise BadRequestError(str(exc)) from exc
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -271,7 +272,7 @@ async def get_blocklist(
|
||||
"""
|
||||
source = await blocklist_service.get_source(blocklist_ctx.db, source_id)
|
||||
if source is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Blocklist source not found.")
|
||||
raise BlocklistSourceNotFoundError(source_id)
|
||||
return source
|
||||
|
||||
|
||||
@@ -307,9 +308,9 @@ async def update_blocklist(
|
||||
enabled=payload.enabled,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
raise BadRequestError(str(exc)) from exc
|
||||
if updated is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Blocklist source not found.")
|
||||
raise BlocklistSourceNotFoundError(source_id)
|
||||
return updated
|
||||
|
||||
|
||||
@@ -335,7 +336,7 @@ async def delete_blocklist(
|
||||
"""
|
||||
deleted = await blocklist_service.delete_source(blocklist_ctx.db, source_id)
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Blocklist source not found.")
|
||||
raise BlocklistSourceNotFoundError(source_id)
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -366,12 +367,9 @@ async def preview_blocklist(
|
||||
"""
|
||||
source = await blocklist_service.get_source(blocklist_ctx.db, source_id)
|
||||
if source is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Blocklist source not found.")
|
||||
raise BlocklistSourceNotFoundError(source_id)
|
||||
|
||||
try:
|
||||
return await blocklist_service.preview_source(source.url, http_session)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f"Could not fetch blocklist: {exc}",
|
||||
) from exc
|
||||
raise BadRequestError(f"Could not fetch blocklist: {exc}") from exc
|
||||
|
||||
@@ -4,7 +4,7 @@ import shlex
|
||||
from typing import Annotated
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, HTTPException, Query, Request, status
|
||||
from fastapi import APIRouter, Query, Request, status
|
||||
|
||||
from app.dependencies import (
|
||||
AuthDep,
|
||||
@@ -12,6 +12,7 @@ from app.dependencies import (
|
||||
Fail2BanStartCommandDep,
|
||||
SettingsServiceContextDep,
|
||||
)
|
||||
from app.exceptions import OperationError
|
||||
from app.models.config import (
|
||||
Fail2BanLogResponse,
|
||||
GlobalConfigResponse,
|
||||
@@ -158,15 +159,12 @@ async def restart_fail2ban(
|
||||
)
|
||||
|
||||
if not restarted:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=(
|
||||
"fail2ban was stopped but did not come back "
|
||||
"online within 10 seconds. "
|
||||
"Check the fail2ban log for initialisation errors. "
|
||||
"Use POST /api/config/jails/{name}/rollback if a "
|
||||
"specific jail is suspect."
|
||||
),
|
||||
raise OperationError(
|
||||
"fail2ban was stopped but did not come back "
|
||||
"online within 10 seconds. "
|
||||
"Check the fail2ban log for initialisation errors. "
|
||||
"Use POST /api/config/jails/{name}/rollback if a "
|
||||
"specific jail is suspect."
|
||||
)
|
||||
log.info("fail2ban_restarted")
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ state so monitoring tools and Docker health checks can observe daemon status
|
||||
without probing the socket directly.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, status
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.dependencies import ServerStatusDep
|
||||
|
||||
@@ -17,7 +17,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, Request
|
||||
from fastapi import APIRouter, Query, Request
|
||||
|
||||
from app.dependencies import (
|
||||
AuthDep,
|
||||
@@ -26,6 +26,7 @@ from app.dependencies import (
|
||||
HistoryServiceContextDep,
|
||||
HttpSessionDep,
|
||||
)
|
||||
from app.exceptions import HistoryNotFoundError
|
||||
from app.models.ban import BanOrigin, TimeRange
|
||||
from app.models.history import HistoryListResponse, IpDetailResponse
|
||||
from app.services import history_service
|
||||
@@ -188,6 +189,6 @@ async def get_ip_history(
|
||||
)
|
||||
|
||||
if detail is None:
|
||||
raise HTTPException(status_code=404, detail=f"No history found for IP {ip!r}.")
|
||||
raise HistoryNotFoundError(ip)
|
||||
|
||||
return detail
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import shlex
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Path, Query, Request, status
|
||||
from fastapi import APIRouter, Path, Query, Request, status
|
||||
|
||||
from app.dependencies import (
|
||||
AppDep,
|
||||
@@ -14,6 +14,7 @@ from app.dependencies import (
|
||||
HealthProbeDep,
|
||||
PendingRecoveryDep,
|
||||
)
|
||||
from app.exceptions import BadRequestError
|
||||
from app.models.config import (
|
||||
ActivateJailRequest,
|
||||
AddLogPathRequest,
|
||||
@@ -258,10 +259,7 @@ async def delete_log_path(
|
||||
try:
|
||||
validate_log_path(log_path)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=str(e),
|
||||
) from e
|
||||
raise BadRequestError(str(e)) from e
|
||||
|
||||
await config_service.delete_log_path(socket_path, name, log_path)
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, HTTPException, Path, status
|
||||
from fastapi import APIRouter, Body, Path, status
|
||||
|
||||
from app.dependencies import (
|
||||
AuthDep,
|
||||
@@ -32,6 +32,7 @@ from app.dependencies import (
|
||||
HttpSessionDep,
|
||||
JailServiceStateDep,
|
||||
)
|
||||
from app.exceptions import BadRequestError
|
||||
from app.models.ban import JailBannedIpsResponse
|
||||
from app.models.jail import (
|
||||
IgnoreIpRequest,
|
||||
@@ -469,15 +470,9 @@ async def get_jail_banned_ips(
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
if page < 1:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="page must be >= 1.",
|
||||
)
|
||||
raise BadRequestError("page must be >= 1.")
|
||||
if not (1 <= page_size <= 100):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="page_size must be between 1 and 100.",
|
||||
)
|
||||
raise BadRequestError("page_size must be between 1 and 100.")
|
||||
|
||||
return await jail_service.get_jail_banned_ips(
|
||||
socket_path=socket_path,
|
||||
|
||||
@@ -8,9 +8,10 @@ return ``409 Conflict``.
|
||||
from __future__ import annotations
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from fastapi import APIRouter, status
|
||||
|
||||
from app.dependencies import AppDep, SettingsDep, SettingsServiceContextDep
|
||||
from app.exceptions import SetupAlreadyCompleteError
|
||||
from app.models.setup import SetupRequest, SetupResponse, SetupStatusResponse, SetupTimezoneResponse
|
||||
from app.services import setup_service
|
||||
from app.utils.runtime_state import update_app_settings
|
||||
@@ -59,13 +60,10 @@ async def post_setup(
|
||||
:class:`~app.models.setup.SetupResponse` on success.
|
||||
|
||||
Raises:
|
||||
HTTPException: 409 if setup has already been completed.
|
||||
SetupAlreadyCompleteError: if setup has already been completed.
|
||||
"""
|
||||
if is_setup_complete_cached(app) or await setup_service.is_setup_complete(settings_ctx.db):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Setup has already been completed.",
|
||||
)
|
||||
raise SetupAlreadyCompleteError()
|
||||
|
||||
await setup_service.run_setup(
|
||||
settings_ctx.db,
|
||||
|
||||
Reference in New Issue
Block a user