From 1a8b81eb1dfd11fd2d95d8ea3025c46947338828 Mon Sep 17 00:00:00 2001 From: goldpulpy Date: Sun, 22 Mar 2026 01:15:49 +0300 Subject: [PATCH 1/6] refactor(api): add comprehensive error handling and validation --- src/app/__main__.py | 10 ++ src/app/api/v1/admin/group.py | 25 ++++ src/app/api/v1/admin/monitor.py | 19 +++ src/app/repositories/group.py | 10 ++ src/app/repositories/monitor.py | 10 ++ src/app/shared/exc_handlers.py | 127 ++++++++++++++++++- src/app/shared/jwt_utils.py | 7 + src/app/shared/middlewares/authentication.py | 27 +++- 8 files changed, 231 insertions(+), 4 deletions(-) diff --git a/src/app/__main__.py b/src/app/__main__.py index 920cbaa..dc0a432 100644 --- a/src/app/__main__.py +++ b/src/app/__main__.py @@ -10,14 +10,20 @@ from fastapi import FastAPI, status from fastapi.staticfiles import StaticFiles from slowapi.errors import RateLimitExceeded +from sqlalchemy.exc import IntegrityError, SQLAlchemyError from app import api, frontend, shared from app.api import docs from app.container import Container +from app.monitoring.scheduler import WorkerSchedulerError from app.shared import config from app.shared.exc_handlers import ( + general_exception_handler, + integrity_error_handler, not_found_handler, rate_limit_exception_handler, + sqlalchemy_error_handler, + worker_scheduler_error_handler, ) from app.shared.log_filters import HealthCheckFilter @@ -66,6 +72,10 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]: app.add_exception_handler(status.HTTP_404_NOT_FOUND, not_found_handler) app.add_exception_handler(RateLimitExceeded, rate_limit_exception_handler) +app.add_exception_handler(IntegrityError, integrity_error_handler) +app.add_exception_handler(SQLAlchemyError, sqlalchemy_error_handler) +app.add_exception_handler(WorkerSchedulerError, worker_scheduler_error_handler) +app.add_exception_handler(Exception, general_exception_handler) api.middlewares.setup_middlewares(app) frontend.middlewares.setup_middlewares(app) diff --git a/src/app/api/v1/admin/group.py b/src/app/api/v1/admin/group.py index 55e03f1..cd7fccc 100644 --- a/src/app/api/v1/admin/group.py +++ b/src/app/api/v1/admin/group.py @@ -130,6 +130,14 @@ async def create_group( ) -> MonitorsGroupResponse: """Create a specific group.""" async with uow_factory() as uow: + existing_group = await uow.groups.find_by_name(create_request.name) + + if existing_group: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Group with this name already exists", + ) + group = await uow.groups.save( MonitorGroupModel(name=create_request.name), ) @@ -178,6 +186,15 @@ async def update_group( detail="Group not found", ) + if group.name != update_request.name: + existing_group = await uow.groups.find_by_name(update_request.name) + + if existing_group: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Group with this name already exists", + ) + group.name = update_request.name group = await uow.groups.save(group) @@ -222,6 +239,14 @@ async def delete_group( detail="Group not found", ) + monitors_in_group = await uow.monitors.find_by_group_id(group_id) + + if monitors_in_group: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Cannot delete group with assigned monitors", + ) + group.is_deleted = True await uow.groups.save(group) diff --git a/src/app/api/v1/admin/monitor.py b/src/app/api/v1/admin/monitor.py index 5388106..d4026fa 100644 --- a/src/app/api/v1/admin/monitor.py +++ b/src/app/api/v1/admin/monitor.py @@ -161,6 +161,14 @@ async def create_monitor( ) -> MonitorResponse: """Create a new monitor.""" async with uow_factory() as uow: + existing_monitor = await uow.monitors.find_by_name(create_request.name) + + if existing_monitor: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Monitor with this name already exists", + ) + if create_request.group_id: group = await uow.groups.find_by_id(create_request.group_id) if not group: @@ -225,6 +233,17 @@ async def update_monitor( detail="Monitor not found", ) + if monitor.name != update_request.name: + existing_monitor = await uow.monitors.find_by_name( + update_request.name, + ) + + if existing_monitor: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Monitor with this name already exists", + ) + if ( update_request.group_id and monitor.group_id != update_request.group_id diff --git a/src/app/repositories/group.py b/src/app/repositories/group.py index c1883d9..9880768 100644 --- a/src/app/repositories/group.py +++ b/src/app/repositories/group.py @@ -39,6 +39,16 @@ async def find_by_id( result = await self._session.execute(stmt) return result.scalar_one_or_none() + async def find_by_name(self, name: str) -> MonitorGroupModel | None: + """Find a group by name.""" + stmt = select(MonitorGroupModel).where( + MonitorGroupModel.name == name, + MonitorGroupModel.is_deleted == False, # noqa: E712 + ) + + result = await self._session.execute(stmt) + return result.scalar_one_or_none() + async def find_all(self) -> list[MonitorGroupModel]: """Find all monitors.""" result = await self._session.execute( diff --git a/src/app/repositories/monitor.py b/src/app/repositories/monitor.py index a33e8a4..05cce7d 100644 --- a/src/app/repositories/monitor.py +++ b/src/app/repositories/monitor.py @@ -39,6 +39,16 @@ async def find_by_id( result = await self._session.execute(stmt) return result.scalar_one_or_none() + async def find_by_name(self, name: str) -> MonitorModel | None: + """Find a monitor by name.""" + stmt = select(MonitorModel).where( + MonitorModel.name == name, + MonitorModel.is_deleted == False, # noqa: E712 + ) + + result = await self._session.execute(stmt) + return result.scalar_one_or_none() + async def find_by_group_id(self, group_id: UUID) -> list[MonitorModel]: """Find monitors by group ID.""" result = await self._session.execute( diff --git a/src/app/shared/exc_handlers.py b/src/app/shared/exc_handlers.py index 64a1279..87e5115 100644 --- a/src/app/shared/exc_handlers.py +++ b/src/app/shared/exc_handlers.py @@ -1,7 +1,8 @@ -"""Frontend handlers.""" +"""Exception handlers.""" from __future__ import annotations +import logging from typing import TYPE_CHECKING from dependency_injector.wiring import Provide, inject @@ -9,12 +10,18 @@ from fastapi.exceptions import HTTPException from fastapi.responses import HTMLResponse, JSONResponse from slowapi.errors import RateLimitExceeded +from sqlalchemy.exc import IntegrityError from app.container import Container +from app.monitoring.scheduler import ( + UnsupportedMonitorTypeError, +) if TYPE_CHECKING: from fastapi.templating import Jinja2Templates +logger = logging.getLogger(__name__) + @inject async def not_found_handler( @@ -58,3 +65,121 @@ async def rate_limit_exception_handler( status_code=500, content={"error": "Internal server error"}, ) + + +async def integrity_error_handler( + request: Request, + exc: Exception, +) -> JSONResponse: + """Handle database integrity errors (unique constraints, foreign keys).""" + if not isinstance(exc, IntegrityError): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={ + "error": "Bad request", + "detail": "Database constraint violation", + }, + ) + + error_msg = str(exc.orig) if hasattr(exc, "orig") else str(exc) + + logger.warning( + "Database integrity error on %s: %s", + request.url.path, + error_msg, + ) + + if "unique" in error_msg.lower() or "duplicate" in error_msg.lower(): + return JSONResponse( + status_code=status.HTTP_409_CONFLICT, + content={ + "error": "Conflict", + "detail": "A resource with this name already exists", + }, + ) + + if "foreign key" in error_msg.lower(): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={ + "error": "Bad request", + "detail": "Referenced resource does not exist", + }, + ) + + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={ + "error": "Bad request", + "detail": "Database constraint violation", + }, + ) + + +async def sqlalchemy_error_handler( + request: Request, + exc: Exception, +) -> JSONResponse: + """Handle general SQLAlchemy errors.""" + logger.exception( + "Database error on %s: %s", + request.url.path, + exc, + ) + + return JSONResponse( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + content={ + "error": "Service unavailable", + "detail": "Database error occurred", + }, + ) + + +async def worker_scheduler_error_handler( + request: Request, + exc: Exception, +) -> JSONResponse: + """Handle worker scheduler errors.""" + logger.exception( + "Worker scheduler error on %s: %s", + request.url.path, + exc, + ) + + if isinstance(exc, UnsupportedMonitorTypeError): + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={ + "error": "Bad request", + "detail": f"Unsupported monitor type: {exc}", + }, + ) + + return JSONResponse( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + content={ + "error": "Service unavailable", + "detail": "Monitoring service error occurred", + }, + ) + + +async def general_exception_handler( + request: Request, + exc: Exception, +) -> JSONResponse: + """Handle unhandled exceptions.""" + logger.exception( + "Unhandled exception on %s: %s", + request.url.path, + exc, + ) + + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "error": "Internal server error", + "detail": "An unexpected error occurred", + }, + ) diff --git a/src/app/shared/jwt_utils.py b/src/app/shared/jwt_utils.py index 97f568d..7ae3140 100644 --- a/src/app/shared/jwt_utils.py +++ b/src/app/shared/jwt_utils.py @@ -43,4 +43,11 @@ def verify_auth_token(token: str) -> dict: token, config.jwt.secret, algorithms=[config.jwt.algorithm], + issuer=config.jwt.issuer, + options={ + "verify_iss": True, + "verify_exp": True, + "verify_nbf": True, + "require": ["exp", "iat", "iss", "sub"], + }, ) diff --git a/src/app/shared/middlewares/authentication.py b/src/app/shared/middlewares/authentication.py index eac0afd..a93e249 100644 --- a/src/app/shared/middlewares/authentication.py +++ b/src/app/shared/middlewares/authentication.py @@ -9,6 +9,8 @@ from jwt.exceptions import ( DecodeError, ExpiredSignatureError, + ImmatureSignatureError, + InvalidIssuerError, InvalidTokenError, ) from starlette.middleware.base import BaseHTTPMiddleware @@ -29,7 +31,10 @@ def __init__(self, app: ASGIApp) -> None: """Initialize authentication middleware.""" super().__init__(app) - def _verify_token(self, request: Request) -> bool: + def _verify_token( # noqa: PLR0911 + self, + request: Request, + ) -> bool: """Verify and extract token data.""" token = request.cookies.get("token") if not token: @@ -38,13 +43,29 @@ def _verify_token(self, request: Request) -> bool: try: data = verify_auth_token(token) - except (ExpiredSignatureError, DecodeError, InvalidTokenError) as e: + + except ExpiredSignatureError: + logger.debug("Token expired for path: %s", request.url.path) + return False + + except InvalidIssuerError: + logger.debug("Invalid token issuer for path: %s", request.url.path) + return False + + except ImmatureSignatureError: + logger.debug("Token not yet valid for path: %s", request.url.path) + return False + + except (DecodeError, InvalidTokenError) as e: logger.debug("Token validation failed: %s", e) return False sub = data.get("sub") if not sub or not isinstance(sub, str): - logger.debug("Invalid or missing user_id in token for path: %s") + logger.debug( + "Invalid or missing 'sub' claim in token for path: %s", + request.url.path, + ) return False return True From a2c277be66659e3d3410a41466e15d73275dcb6b Mon Sep 17 00:00:00 2001 From: goldpulpy Date: Sun, 22 Mar 2026 01:31:43 +0300 Subject: [PATCH 2/6] refactor(api): improve HTTP validation and error handling --- src/app/api/models/monitor.py | 2 +- src/app/api/models/validators/http.py | 64 +++++++++++++++++--- src/app/api/v1/admin/auth.py | 6 +- src/app/shared/middlewares/authentication.py | 3 +- 4 files changed, 63 insertions(+), 12 deletions(-) diff --git a/src/app/api/models/monitor.py b/src/app/api/models/monitor.py index a0b8394..d7e782b 100644 --- a/src/app/api/models/monitor.py +++ b/src/app/api/models/monitor.py @@ -64,7 +64,7 @@ class MonitorRequest(BaseModel): @model_validator(mode="before") @classmethod def validate(cls, values: dict) -> dict: - """Validate HTTP endpoint.""" + """Validate monitor configuration.""" monitor_type = values.get("type") if monitor_type == MonitorType.HTTP: diff --git a/src/app/api/models/validators/http.py b/src/app/api/models/validators/http.py index 67102b1..f21ad72 100644 --- a/src/app/api/models/validators/http.py +++ b/src/app/api/models/validators/http.py @@ -2,7 +2,10 @@ from urllib.parse import urlparse -from fastapi import HTTPException +from fastapi import HTTPException, status + +HTTP_ERROR_MIN = 100 +HTTP_ERROR_MAX = 599 def _validate_endpoint(endpoint: str) -> None: @@ -11,17 +14,20 @@ def _validate_endpoint(endpoint: str) -> None: parsed = urlparse(endpoint) if parsed.scheme not in ["http", "https"]: - raise HTTPException(status_code=400, detail="Invalid URL scheme") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid URL scheme", + ) if not parsed.netloc: raise HTTPException( - status_code=400, + status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid endpoint: missing domain", ) except ValueError as e: raise HTTPException( - status_code=400, + status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid endpoint URL format", ) from e @@ -31,13 +37,56 @@ def _validate_method(method: str) -> None: allowed_methods = ["GET", "POST", "PUT", "DELETE"] if method.upper() not in allowed_methods: raise HTTPException( - status_code=400, + status_code=status.HTTP_400_BAD_REQUEST, detail=( f"Invalid HTTP method. Allowed: {', '.join(allowed_methods)}", ), ) +def validate_codes(values: dict) -> None: + """Validate HTTP codes.""" + expected_response_code = values.get("expected_response_code") + if expected_response_code is not None and not ( + HTTP_ERROR_MIN <= expected_response_code <= HTTP_ERROR_MAX + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="expected_response_code must be between 100 and 599", + ) + + latency_threshold_ms = values.get("latency_threshold_ms") + if latency_threshold_ms is not None and latency_threshold_ms <= 0: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="latency_threshold_ms must be greater than 0", + ) + + error_mapping = values.get("error_mapping") + if error_mapping: + invalid_codes = [] + + for code in error_mapping: + try: + code_int = int(code) + + except (TypeError, ValueError): + invalid_codes.append(code) + continue + + if code_int < HTTP_ERROR_MIN or code_int > HTTP_ERROR_MAX: + invalid_codes.append(code) + + if invalid_codes: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + "error_mapping keys must be HTTP status codes between" + f" 100 and 599; invalid: {invalid_codes}" + ), + ) + + def validate_http_monitor(values: dict) -> None: """Validate HTTP monitor configuration.""" endpoint = values.get("endpoint") @@ -45,15 +94,16 @@ def validate_http_monitor(values: dict) -> None: if not endpoint: raise HTTPException( - status_code=400, + status_code=status.HTTP_400_BAD_REQUEST, detail="Missing endpoint for HTTP monitor", ) if not method: raise HTTPException( - status_code=400, + status_code=status.HTTP_400_BAD_REQUEST, detail="Missing method for HTTP monitor", ) _validate_endpoint(endpoint) _validate_method(method) + validate_codes(values) diff --git a/src/app/api/v1/admin/auth.py b/src/app/api/v1/admin/auth.py index d786c96..52375de 100644 --- a/src/app/api/v1/admin/auth.py +++ b/src/app/api/v1/admin/auth.py @@ -120,10 +120,10 @@ async def logout( ) -> LogoutResponse: """Admin logout.""" response.delete_cookie( - key="token", + key=config.cookie.key, httponly=True, - samesite="none" if config.app.is_production else "lax", - secure=config.app.is_production, + samesite="lax", + secure=config.app.https, ) return LogoutResponse( diff --git a/src/app/shared/middlewares/authentication.py b/src/app/shared/middlewares/authentication.py index a93e249..ec36ee2 100644 --- a/src/app/shared/middlewares/authentication.py +++ b/src/app/shared/middlewares/authentication.py @@ -15,6 +15,7 @@ ) from starlette.middleware.base import BaseHTTPMiddleware +from app.shared import config from app.shared.jwt_utils import verify_auth_token if TYPE_CHECKING: @@ -36,7 +37,7 @@ def _verify_token( # noqa: PLR0911 request: Request, ) -> bool: """Verify and extract token data.""" - token = request.cookies.get("token") + token = request.cookies.get(config.cookie.key) if not token: logger.debug("Missing token for %s", request.url.path) return False From ede80d3c232ead1c2ce42ece29ad3d8a120c9cd5 Mon Sep 17 00:00:00 2001 From: goldpulpy Date: Sun, 22 Mar 2026 01:34:05 +0300 Subject: [PATCH 3/6] refactor(auth): replace hardcoded HTTP status codes with FastAPI status constants --- src/app/api/middlewares/authentication.py | 3 ++- src/app/frontend/middlewares/authentication.py | 3 ++- src/app/shared/exc_handlers.py | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/api/middlewares/authentication.py b/src/app/api/middlewares/authentication.py index aa3e747..8730462 100644 --- a/src/app/api/middlewares/authentication.py +++ b/src/app/api/middlewares/authentication.py @@ -4,6 +4,7 @@ import logging from typing import TYPE_CHECKING, ClassVar +from fastapi import status from fastapi.responses import JSONResponse @@ -45,7 +46,7 @@ async def dispatch( if not self._verify_token(request): return JSONResponse( content={"detail": "Unauthorized"}, - status_code=401, + status_code=status.HTTP_401_UNAUTHORIZED, ) return await call_next(request) diff --git a/src/app/frontend/middlewares/authentication.py b/src/app/frontend/middlewares/authentication.py index 3aa034b..eded4b9 100644 --- a/src/app/frontend/middlewares/authentication.py +++ b/src/app/frontend/middlewares/authentication.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, ClassVar from fastapi.responses import RedirectResponse +from fastapi import status from app.shared import config from app.shared.middlewares import BaseAuthMiddleware @@ -50,7 +51,7 @@ async def dispatch( if not self._verify_token(request): return RedirectResponse( url=f"/{config.admin.safe_path}/login", - status_code=302, + status_code=status.HTTP_302_FOUND, ) return await call_next(request) diff --git a/src/app/shared/exc_handlers.py b/src/app/shared/exc_handlers.py index 87e5115..fc215c3 100644 --- a/src/app/shared/exc_handlers.py +++ b/src/app/shared/exc_handlers.py @@ -54,7 +54,7 @@ async def rate_limit_exception_handler( """Rate limit handler.""" if isinstance(exc, RateLimitExceeded): return JSONResponse( - status_code=429, + status_code=status.HTTP_429_TOO_MANY_REQUESTS, content={ "error": "Too many requests", "detail": f"Rate limit exceeded: {exc.detail}", @@ -62,7 +62,7 @@ async def rate_limit_exception_handler( ) return JSONResponse( - status_code=500, + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={"error": "Internal server error"}, ) From 6a7f665dce69c0a20115486ac7b4fc4bb501ba0f Mon Sep 17 00:00:00 2001 From: goldpulpy Date: Sun, 22 Mar 2026 01:35:00 +0300 Subject: [PATCH 4/6] refactor(auth): reorder import statements for consistency --- src/app/api/middlewares/authentication.py | 2 +- src/app/frontend/middlewares/authentication.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/api/middlewares/authentication.py b/src/app/api/middlewares/authentication.py index 8730462..225c3f6 100644 --- a/src/app/api/middlewares/authentication.py +++ b/src/app/api/middlewares/authentication.py @@ -4,8 +4,8 @@ import logging from typing import TYPE_CHECKING, ClassVar -from fastapi import status +from fastapi import status from fastapi.responses import JSONResponse from app.shared import config diff --git a/src/app/frontend/middlewares/authentication.py b/src/app/frontend/middlewares/authentication.py index eded4b9..9cb9030 100644 --- a/src/app/frontend/middlewares/authentication.py +++ b/src/app/frontend/middlewares/authentication.py @@ -5,8 +5,8 @@ import logging from typing import TYPE_CHECKING, ClassVar -from fastapi.responses import RedirectResponse from fastapi import status +from fastapi.responses import RedirectResponse from app.shared import config from app.shared.middlewares import BaseAuthMiddleware From 2c75c9c26fcfcfbbb988dad2e20e6b8e32ba16fe Mon Sep 17 00:00:00 2001 From: goldpulpy Date: Sun, 22 Mar 2026 01:50:39 +0300 Subject: [PATCH 5/6] refactor(api): improve HTTP validation and error handling --- pyproject.toml | 2 +- src/app/api/models/validators/http.py | 4 ++-- src/app/shared/middlewares/authentication.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 12b404c..d86c26a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ unsafe-fixes = true [tool.ruff.lint] select = ["ALL"] -ignore = ["INP001", "ARG001"] +ignore = ["INP001", "ARG001", "PLR0911"] [tool.pyright] include = ["src"] diff --git a/src/app/api/models/validators/http.py b/src/app/api/models/validators/http.py index f21ad72..0404689 100644 --- a/src/app/api/models/validators/http.py +++ b/src/app/api/models/validators/http.py @@ -44,7 +44,7 @@ def _validate_method(method: str) -> None: ) -def validate_codes(values: dict) -> None: +def _validate_codes(values: dict) -> None: """Validate HTTP codes.""" expected_response_code = values.get("expected_response_code") if expected_response_code is not None and not ( @@ -106,4 +106,4 @@ def validate_http_monitor(values: dict) -> None: _validate_endpoint(endpoint) _validate_method(method) - validate_codes(values) + _validate_codes(values) diff --git a/src/app/shared/middlewares/authentication.py b/src/app/shared/middlewares/authentication.py index ec36ee2..958daf6 100644 --- a/src/app/shared/middlewares/authentication.py +++ b/src/app/shared/middlewares/authentication.py @@ -32,12 +32,13 @@ def __init__(self, app: ASGIApp) -> None: """Initialize authentication middleware.""" super().__init__(app) - def _verify_token( # noqa: PLR0911 + def _verify_token( self, request: Request, ) -> bool: """Verify and extract token data.""" token = request.cookies.get(config.cookie.key) + if not token: logger.debug("Missing token for %s", request.url.path) return False From 7bfe148641badc325208f6a5dd9bee4a1e621e93 Mon Sep 17 00:00:00 2001 From: goldpulpy Date: Sun, 22 Mar 2026 02:05:28 +0300 Subject: [PATCH 6/6] refactor(api): improve HTTP validation and error handling --- src/app/api/models/validators/http.py | 10 +++++----- src/app/monitoring/workers/http.py | 7 ++++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/app/api/models/validators/http.py b/src/app/api/models/validators/http.py index 0404689..805bb49 100644 --- a/src/app/api/models/validators/http.py +++ b/src/app/api/models/validators/http.py @@ -48,18 +48,18 @@ def _validate_codes(values: dict) -> None: """Validate HTTP codes.""" expected_response_code = values.get("expected_response_code") if expected_response_code is not None and not ( - HTTP_ERROR_MIN <= expected_response_code <= HTTP_ERROR_MAX + HTTP_ERROR_MIN <= int(expected_response_code) <= HTTP_ERROR_MAX ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="expected_response_code must be between 100 and 599", + detail="Expected response code must be between 100 and 599", ) latency_threshold_ms = values.get("latency_threshold_ms") - if latency_threshold_ms is not None and latency_threshold_ms <= 0: + if latency_threshold_ms is not None and int(latency_threshold_ms) <= 0: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="latency_threshold_ms must be greater than 0", + detail="Latency threshold must be greater than 0", ) error_mapping = values.get("error_mapping") @@ -81,7 +81,7 @@ def _validate_codes(values: dict) -> None: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=( - "error_mapping keys must be HTTP status codes between" + "Error mapping keys must be HTTP status codes between" f" 100 and 599; invalid: {invalid_codes}" ), ) diff --git a/src/app/monitoring/workers/http.py b/src/app/monitoring/workers/http.py index b25cae5..23539bd 100644 --- a/src/app/monitoring/workers/http.py +++ b/src/app/monitoring/workers/http.py @@ -1,5 +1,6 @@ """HTTP worker for endpoint monitoring.""" +import json import logging import httpx @@ -97,7 +98,11 @@ async def _execute_request(self, client: httpx.AsyncClient) -> Response: kwargs = {} if self._config.request_body: - kwargs["json"] = self._config.request_body + try: + kwargs["json"] = json.loads(self._config.request_body) + + except json.JSONDecodeError: + kwargs["data"] = self._config.request_body response = await client.request( method,