Skip to content

Commit ce2431f

Browse files
Wrap input validation error in RFC9457 response (#311)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 3e9042a commit ce2431f

4 files changed

Lines changed: 37 additions & 9 deletions

File tree

src/core/errors.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from http import HTTPStatus
88

99
from fastapi import Request
10+
from fastapi.exceptions import RequestValidationError
1011
from fastapi.responses import JSONResponse
1112

1213
# =============================================================================
@@ -89,6 +90,27 @@ def problem_detail_exception_handler(
8990
)
9091

9192

93+
def validation_exception_handler(
94+
request: Request, # noqa: ARG001
95+
exc: RequestValidationError,
96+
) -> JSONResponse:
97+
"""FastAPI exception handler for RequestValidationError.
98+
99+
Returns a RFC 9457 compliant response for input validation failures.
100+
"""
101+
return JSONResponse(
102+
status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
103+
content={
104+
"type": "https://openml.org/problems/validation-error",
105+
"title": "Validation Error",
106+
"status": HTTPStatus.UNPROCESSABLE_ENTITY,
107+
"detail": "Input validation failed.",
108+
"errors": exc.errors(),
109+
},
110+
media_type="application/problem+json",
111+
)
112+
113+
92114
# =============================================================================
93115
# Dataset Errors
94116
# =============================================================================

src/main.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@
77

88
import uvicorn
99
from fastapi import FastAPI
10+
from fastapi.exceptions import RequestValidationError
1011
from loguru import logger
1112

1213
from config import load_configuration
13-
from core.errors import ProblemDetailError, problem_detail_exception_handler
14+
from core.errors import (
15+
ProblemDetailError,
16+
problem_detail_exception_handler,
17+
validation_exception_handler,
18+
)
1419
from core.logging import (
1520
add_request_context_to_log,
1621
log_request_duration,
@@ -87,6 +92,7 @@ def create_api(configuration_file: Path | None = None) -> FastAPI:
8792
app.middleware("http")(add_request_context_to_log)
8893

8994
app.add_exception_handler(ProblemDetailError, problem_detail_exception_handler) # type: ignore[arg-type]
95+
app.add_exception_handler(RequestValidationError, validation_exception_handler) # type: ignore[arg-type]
9096

9197
logger.info("Adding routers to app")
9298
app.include_router(datasets_router)

tests/routers/openml/dataset_tag_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ async def test_dataset_tag_invalid_tag_is_rejected(
4242
)
4343

4444
assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
45-
assert response.json()["detail"][0]["loc"] == ["body", "tag"]
45+
assert response.json()["errors"][0]["loc"] == ["body", "tag"]
4646

4747

4848
# ── Direct call tests: tag_dataset ──

tests/routers/openml/task_list_test.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,9 @@ async def test_list_tasks_invalid_pagination_type(
135135
)
136136
assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
137137
# Verify that the error points to the correct field
138-
detail = response.json()["detail"][0]
139-
assert detail["loc"][-2:] == ["pagination", expected_field]
140-
assert detail["type"] in {"type_error.integer", "int_parsing", "int_type"}
138+
error = response.json()["errors"][0]
139+
assert error["loc"][-2:] == ["pagination", expected_field]
140+
assert error["type"] in {"type_error.integer", "int_parsing", "int_type"}
141141

142142

143143
@pytest.mark.parametrize(
@@ -150,8 +150,8 @@ async def test_list_tasks_invalid_range(value: str, py_api: httpx.AsyncClient) -
150150
response = await py_api.post("/tasks/list", json={"number_instances": value})
151151
assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
152152
# Verify the error is for the correct field
153-
detail = response.json()["detail"][0]
154-
assert detail["loc"][-1] == "number_instances"
153+
error = response.json()["errors"][0]
154+
assert error["loc"][-1] == "number_instances"
155155

156156

157157
@pytest.mark.parametrize(
@@ -171,9 +171,9 @@ async def test_list_tasks_invalid_inputs(
171171
response = await py_api.post("/tasks/list", json=payload)
172172
assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
173173
# Ensure we are failing for the field we provided
174-
detail = response.json()["detail"][0]
174+
error = response.json()["errors"][0]
175175
expected_field = next(iter(payload))
176-
assert detail["loc"][-1] == expected_field
176+
assert error["loc"][-1] == expected_field
177177

178178

179179
async def test_list_tasks_no_results_api_mapping(py_api: httpx.AsyncClient) -> None:

0 commit comments

Comments
 (0)