[Bugfix] Fall back to Pydantic loc for param in validation errors (#46038)
Signed-off-by: professorsab <135441198+professorsab@users.noreply.github.com> Co-authored-by: Mahad Durrani <114791389+mahadrehmann@users.noreply.github.com>
Muhammad Fawaz committed
Jun 19, 2026 at 16:11 UTC
69bdd345428408a2fdf745e225c87defbc2c07d0
2 files changed
+71
tests/entrypoints/serve/utils/test_server_utils.py
new
+65
@@ -0,0 +1,65 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Tests that validation_exception_handler populates the `param` field
4
+in its error response using the Pydantic error's `loc`, even when no
5
+custom VLLMValidationError context is present.
6
+
7
+Previously, `param` was only populated for errors carrying a custom
8
+VLLMValidationError in their Pydantic `ctx`. Plain validation failures
9
+(missing fields, wrong types) left `param` as None, even though the
10
+field name was readily available from `error['loc']`.
11
+"""
12
+
13
+import json
14
+from types import SimpleNamespace
15
+
16
+import pytest
17
+from fastapi.exceptions import RequestValidationError
18
+
19
+from vllm.entrypoints.serve.utils.server_utils import validation_exception_handler
20
+
21
+
22
+def _fake_request(log_error_stack: bool = False) -> SimpleNamespace:
23
+ """Minimal stand-in for a FastAPI Request - just enough for the
24
+ handler to read req.app.state.args.log_error_stack."""
25
+ return SimpleNamespace(
26
+ app=SimpleNamespace(
27
+ state=SimpleNamespace(args=SimpleNamespace(log_error_stack=log_error_stack))
28
+ ),
29
+ state=SimpleNamespace(), # no request_metadata -> hasattr(...) is False
30
+ )
31
+
32
+
33
+class TestValidationErrorParamFallback:
34
+ """Ensure `param` falls back to the Pydantic error's `loc` when no
35
+ custom VLLMValidationError context is present."""
36
+
37
+ @pytest.mark.parametrize(
38
+ ("error_type", "msg"),
39
+ [
40
+ ("missing", "Field required"),
41
+ ("list_type", "Input should be a valid list"),
42
+ ],
43
+ ids=["missing-field", "wrong-type"],
44
+ )
45
+ @pytest.mark.asyncio
46
+ async def test_param_falls_back_to_loc(self, error_type: str, msg: str):
47
+ errors = [{"type": error_type, "loc": ("body", "messages"), "msg": msg}]
48
+ exc = RequestValidationError(errors)
49
+
50
+ response = await validation_exception_handler(_fake_request(), exc)
51
+ body = json.loads(response.body)
52
+
53
+ assert body["error"]["param"] == "body.messages"
54
+
55
+ @pytest.mark.asyncio
56
+ async def test_param_fallback_does_not_crash_on_non_dict_error(self):
57
+ """Schemathesis fuzzing found that errors[0] isn't always a dict.
58
+ The fallback must not crash in that case - it should just leave
59
+ param as None instead of raising."""
60
+ exc = RequestValidationError(["some unexpected non-dict error"])
61
+
62
+ response = await validation_exception_handler(_fake_request(), exc)
63
+ body = json.loads(response.body)
64
+
65
+ assert body["error"]["param"] is None
vllm/entrypoints/serve/utils/server_utils.py
+6
@@ -427,6 +427,12 @@ async def validation_exception_handler(req: Request, exc: RequestValidationError
427
param = ctx_error.parameter
428
break
429
430
+ if param is None and errors:
431
+ first_error = errors[0]
432
+ loc = first_error.get("loc") if isinstance(first_error, dict) else None
433
+ if loc:
434
+ param = ".".join(str(part) for part in loc)
435
+
436
exc_str = str(exc)
437
errors_str = str(errors)
438