[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