Skip to content

Commit 06e4cb2

Browse files
authored
Fix validation error related to versioning (#686)
1 parent a6a7e86 commit 06e4cb2

File tree

3 files changed

+29
-22
lines changed

3 files changed

+29
-22
lines changed

backend/app/core/exception_handlers.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ def _is_branch_identifier(part: str) -> bool:
1818
return bool(part and isinstance(part, str) and _BRANCH_PATTERN.search(part))
1919

2020

21-
def _filter_union_branch_errors(errors: list[dict]) -> list[dict]:
22-
"""When a field is a Union type, pydantic returns errors for every possible branch.
21+
def _sanitize_validation_errors(errors: list[dict]) -> list[dict]:
22+
"""Sanitize pydantic validation errors.
2323
24-
This function picks the branch where the validation error happend.
24+
Filters union branch noise (keeps only the relevant branch),
25+
strips internal fields, returning only loc, msg, and type.
2526
"""
2627
try:
2728
branch_errors: dict[str, dict[str, list[dict]]] = defaultdict(
@@ -74,7 +75,11 @@ def _filter_union_branch_errors(errors: list[dict]) -> list[dict]:
7475
seen_errors.add(error_key)
7576
unique_errors.append(error)
7677

77-
return unique_errors or errors
78+
sanitized = [
79+
{k: v for k, v in err.items() if k in ("loc", "msg", "type")}
80+
for err in (unique_errors or errors)
81+
]
82+
return sanitized
7883
except Exception:
7984
return errors
8085

@@ -84,7 +89,7 @@ def register_exception_handlers(app: FastAPI) -> None:
8489
async def validation_error_handler(
8590
request: Request, exc: RequestValidationError
8691
) -> JSONResponse:
87-
errors = _filter_union_branch_errors(exc.errors())
92+
errors = _sanitize_validation_errors(exc.errors())
8893
return JSONResponse(
8994
status_code=HTTP_422_UNPROCESSABLE_ENTITY,
9095
content=APIResponse.failure_response(errors).model_dump(),
@@ -94,9 +99,12 @@ async def validation_error_handler(
9499
async def http_exception_handler(
95100
request: Request, exc: HTTPException
96101
) -> JSONResponse:
102+
detail = exc.detail
103+
if isinstance(detail, list):
104+
detail = _sanitize_validation_errors(detail)
97105
return JSONResponse(
98106
status_code=exc.status_code,
99-
content=APIResponse.failure_response(exc.detail).model_dump(),
107+
content=APIResponse.failure_response(detail).model_dump(),
100108
)
101109

102110
@app.exception_handler(Exception)

backend/app/crud/config/version.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,13 @@ def create_or_raise(self, version_create: ConfigVersionUpdate) -> ConfigVersion:
6464
try:
6565
validated_blob = ConfigBlob.model_validate(merged_config)
6666
except ValidationError as e:
67+
validation_errors = e.errors()
6768
logger.error(
68-
f"[ConfigVersionCrud.create_from_partial] Validation failed | "
69-
f"{{'config_id': '{self.config_id}', 'error': '{str(e)}'}}"
70-
)
71-
raise HTTPException(
72-
status_code=400,
73-
detail=f"Invalid config after merge: {str(e)}",
69+
f"[ConfigVersionCrud.create_or_raise] Validation failed | "
70+
f"{{'config_id': '{self.config_id}', 'error_count': {len(validation_errors)}, "
71+
f"'fields': {['.'.join(str(part) for part in err['loc']) for err in validation_errors]}}}"
7472
)
73+
raise HTTPException(status_code=400, detail=validation_errors)
7574

7675
try:
7776
next_version = self._get_next_version(self.config_id)

backend/app/tests/core/test_exception_handlers.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
from fastapi.testclient import TestClient
22

33
from app.core.config import settings
4-
from app.core.exception_handlers import _filter_union_branch_errors
4+
from app.core.exception_handlers import _sanitize_validation_errors
55
from app.tests.utils.auth import TestAuthContext
66

77

8-
class TestFilterUnionBranchErrors:
9-
"""Unit tests for _filter_union_branch_errors."""
8+
class TestSanitizeValidationErrors:
9+
"""Unit tests for _sanitize_validation_errors."""
1010

1111
def test_non_union_errors_pass_through(self) -> None:
1212
errors = [
1313
{"type": "missing", "loc": ("body", "name"), "msg": "Field required"},
1414
]
15-
assert _filter_union_branch_errors(errors) == errors
15+
assert _sanitize_validation_errors(errors) == errors
1616

1717
def test_picks_branch_with_fewer_literal_errors(self) -> None:
1818
errors = [
@@ -37,7 +37,7 @@ def test_picks_branch_with_fewer_literal_errors(self) -> None:
3737
"msg": "Field required",
3838
},
3939
]
40-
result = _filter_union_branch_errors(errors)
40+
result = _sanitize_validation_errors(errors)
4141
assert len(result) == 2
4242
for err in result:
4343
assert "NativeConfig" not in err["loc"]
@@ -56,7 +56,7 @@ def test_tied_branches_keep_both_and_dedup(self) -> None:
5656
"msg": "Field required",
5757
},
5858
]
59-
result = _filter_union_branch_errors(errors)
59+
result = _sanitize_validation_errors(errors)
6060
assert len(result) == 1
6161
assert result[0]["loc"] == ("body", "c", "x")
6262

@@ -74,7 +74,7 @@ def test_strips_branch_identifiers_from_loc(self) -> None:
7474
"msg": "Field required",
7575
}
7676
]
77-
result = _filter_union_branch_errors(errors)
77+
result = _sanitize_validation_errors(errors)
7878
assert result[0]["loc"] == ("body", "cfg", "completion", "params")
7979

8080
def test_non_union_preserved_with_union(self) -> None:
@@ -91,17 +91,17 @@ def test_non_union_preserved_with_union(self) -> None:
9191
"msg": "Field required",
9292
},
9393
]
94-
result = _filter_union_branch_errors(errors)
94+
result = _sanitize_validation_errors(errors)
9595
assert len(result) == 2
9696
locs = [r["loc"] for r in result]
9797
assert ("body", "name") in locs
9898

9999
def test_empty_list(self) -> None:
100-
assert _filter_union_branch_errors([]) == []
100+
assert _sanitize_validation_errors([]) == []
101101

102102
def test_fallback_on_malformed_input(self) -> None:
103103
malformed = [None, 42] # type: ignore[list-item]
104-
result = _filter_union_branch_errors(malformed)
104+
result = _sanitize_validation_errors(malformed)
105105
assert result == malformed
106106

107107

0 commit comments

Comments
 (0)