diff --git a/core/nurse_scheduling/loader.py b/core/nurse_scheduling/loader.py index 484cca84..fa984fbd 100644 --- a/core/nurse_scheduling/loader.py +++ b/core/nurse_scheduling/loader.py @@ -24,6 +24,11 @@ yaml = YAML(typ='safe') + +class InputError(ValueError): + """Raised when user-provided input fails validation (HTTP 400 candidate).""" + pass + def _load_yaml(content: bytes) -> Dict[str, Any]: """Load YAML from bytes content. @@ -48,5 +53,17 @@ def load_data(content: bytes) -> NurseSchedulingData: Returns: NurseSchedulingData: The validated scheduling data """ - data = _load_yaml(content) - return NurseSchedulingData(**data) + try: + data = _load_yaml(content) + except Exception as e: + raise InputError(f"Invalid YAML: {e}") from e + if not isinstance(data, dict): + raise InputError( + "Invalid YAML input: expected a mapping (key-value object) at the top level, " + f"but got {type(data).__name__}." + ) + try: + return NurseSchedulingData(**data) + except TypeError as e: + # e.g. unexpected/missing keyword arguments from a malformed mapping + raise InputError(f"Invalid scheduling data: {e}") from e diff --git a/core/nurse_scheduling/serve.py b/core/nurse_scheduling/serve.py index 98f4957a..3576d96c 100644 --- a/core/nurse_scheduling/serve.py +++ b/core/nurse_scheduling/serve.py @@ -25,6 +25,7 @@ from fastapi.responses import StreamingResponse from fastapi.middleware.cors import CORSMiddleware from . import scheduler, exporter +from .loader import InputError import sentry_sdk sentry_sdk.init( @@ -146,6 +147,13 @@ async def optimize_and_export_xlsx( solver=solver, ) + except InputError as e: + # Bad user input -> HTTP 400 + logging.warning(f"Invalid input for optimization: {str(e)}") + raise HTTPException( + status_code=400, + detail=f"Invalid input: {str(e)}" + ) except Exception as e: # TODO(security): Returning the error message to the client may be a security risk logging.error(f"Error during optimization: {str(e)}")