[Bugfix][Frontend] Guard remaining before-validators against non-object JSON bodies (#52528)
Signed-off-by: Kaif <kaifkohari10@gmail.com>
Kaif Kohari committed
Aug 17, 2026 at 03:27 UTC
a6a2a93f9b9f3b2c155b50c8c6f2badf41cbfd66
7 files changed
+97
tests/entrypoints/unit_tests/test_non_object_body_validation.py
new
+71
@@ -0,0 +1,71 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+"""Non-object JSON bodies must fail validation cleanly (4xx), not AttributeError (500).
5
+
6
+mode=before validators that call data.get(...) without an isinstance(data, dict)
7
+guard raise AttributeError for string/list/scalar bodies and surface as HTTP 500.
8
+
9
+This extends the chat completion coverage added in #51654 to the remaining
10
+request models whose before-validators were missing the same guard.
11
+"""
12
+
13
+import pytest
14
+from pydantic import ValidationError
15
+
16
+from vllm.entrypoints.openai.completion.protocol import CompletionRequest
17
+from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
18
+from vllm.entrypoints.pooling.classify.protocol import ClassificationChatRequest
19
+from vllm.entrypoints.pooling.embed.protocol import EmbeddingChatRequest
20
+from vllm.entrypoints.pooling.pooling.protocol import PoolingChatRequest
21
+from vllm.entrypoints.serve.tokenize.protocol import TokenizeChatRequest
22
+from vllm.entrypoints.speech_to_text.transcription.protocol import TranscriptionRequest
23
+from vllm.entrypoints.speech_to_text.translation.protocol import TranslationRequest
24
+from vllm.exceptions import VLLMValidationError
25
+
26
+pytestmark = pytest.mark.skip_global_cleanup
27
+
28
+REQUEST_MODELS = [
29
+ CompletionRequest,
30
+ ResponsesRequest,
31
+ EmbeddingChatRequest,
32
+ ClassificationChatRequest,
33
+ PoolingChatRequest,
34
+ TokenizeChatRequest,
35
+ TranscriptionRequest,
36
+ TranslationRequest,
37
+]
38
+
39
+
40
+@pytest.mark.parametrize("request_model", REQUEST_MODELS, ids=lambda m: m.__name__)
41
+@pytest.mark.parametrize(
42
+ "payload",
43
+ [
44
+ "this is not valid json{{{",
45
+ ["not", "an", "object"],
46
+ 42,
47
+ None,
48
+ True,
49
+ ],
50
+)
51
+def test_request_models_reject_non_object_body(request_model, payload):
52
+ with pytest.raises(ValidationError):
53
+ request_model.model_validate(payload)
54
+
55
+
56
+def test_completion_request_still_validates_dict_bodies():
57
+ """The guard must not swallow real field-level errors on object bodies."""
58
+ with pytest.raises(VLLMValidationError, match="prompt"):
59
+ CompletionRequest.model_validate({"model": "qwen", "prompt": ""})
60
+
61
+
62
+def test_tokenize_chat_request_still_validates_dict_bodies():
63
+ with pytest.raises(VLLMValidationError, match="add_generation_prompt"):
64
+ TokenizeChatRequest.model_validate(
65
+ {
66
+ "model": "qwen",
67
+ "messages": [{"role": "user", "content": "hello"}],
68
+ "continue_final_message": True,
69
+ "add_generation_prompt": True,
70
+ }
71
+ )
vllm/entrypoints/openai/completion/protocol.py
+12
@@ -413,6 +413,8 @@ class CompletionRequest(OpenAIBaseModel):
413
@model_validator(mode="before")
414
@classmethod
415
def validate_response_format(cls, data):
416
+ if not isinstance(data, dict):
417
+ return data
418
response_format = data.get("response_format")
419
if response_format is None:
420
return data
@@ -444,6 +446,8 @@ class CompletionRequest(OpenAIBaseModel):
446
@model_validator(mode="before")
447
@classmethod
448
def check_structured_outputs_count(cls, data):
449
+ if not isinstance(data, dict):
450
+ return data
451
if data.get("structured_outputs", None) is None:
452
return data
453
@@ -472,6 +476,8 @@ class CompletionRequest(OpenAIBaseModel):
476
@model_validator(mode="before")
477
@classmethod
478
def check_logprobs(cls, data):
479
+ if not isinstance(data, dict):
480
+ return data
481
if data.get("logprob_token_ids") and data.get("use_beam_search"):
482
raise VLLMValidationError(
483
"`logprob_token_ids` is not supported with beam search.",
@@ -532,6 +538,8 @@ class CompletionRequest(OpenAIBaseModel):
538
@model_validator(mode="before")
539
@classmethod
540
def validate_stream_options(cls, data):
541
+ if not isinstance(data, dict):
542
+ return data
543
if data.get("stream_options") and not data.get("stream"):
544
raise VLLMValidationError(
545
"Stream options can only be defined when `stream=True`.",
@@ -543,6 +551,8 @@ class CompletionRequest(OpenAIBaseModel):
551
@model_validator(mode="before")
552
@classmethod
553
def validate_prompt_and_prompt_embeds(cls, data):
554
+ if not isinstance(data, dict):
555
+ return data
556
prompt = data.get("prompt")
557
prompt_embeds = data.get("prompt_embeds")
558
@@ -562,6 +572,8 @@ class CompletionRequest(OpenAIBaseModel):
572
@model_validator(mode="before")
573
@classmethod
574
def validate_prompt_list_length(cls, data):
575
+ if not isinstance(data, dict):
576
+ return data
577
max_prompts = envs.VLLM_MAX_COMPLETION_PROMPTS
578
579
prompt = data.get("prompt")
vllm/entrypoints/openai/responses/protocol.py
+6
@@ -467,6 +467,8 @@ class ResponsesRequest(OpenAIBaseModel):
467
@model_validator(mode="before")
468
@classmethod
469
def validate_background(cls, data):
470
+ if not isinstance(data, dict):
471
+ return data
472
if not data.get("background"):
473
return data
474
if not data.get("store", True):
@@ -479,6 +481,8 @@ class ResponsesRequest(OpenAIBaseModel):
481
@model_validator(mode="before")
482
@classmethod
483
def validate_prompt(cls, data):
484
+ if not isinstance(data, dict):
485
+ return data
486
if data.get("prompt") is not None:
487
raise VLLMValidationError(
488
"prompt template is not supported", parameter="prompt"
@@ -499,6 +503,8 @@ class ResponsesRequest(OpenAIBaseModel):
503
504
Invalid structures are left for Pydantic to reject.
505
"""
506
+ if not isinstance(data, dict):
507
+ return data
508
input_data = data.get("input")
509
510
# Early return for None, strings, or bytes
vllm/entrypoints/pooling/base/protocol.py
+2
@@ -239,6 +239,8 @@ class ChatRequestOptionsMixin(OpenAIBaseModel):
239
@model_validator(mode="before")
240
@classmethod
241
def check_generation_prompt(cls, data):
242
+ if not isinstance(data, dict):
243
+ return data
244
if data.get("continue_final_message") and data.get("add_generation_prompt"):
245
raise VLLMValidationError(
246
"Cannot set both `continue_final_message` and "
vllm/entrypoints/serve/tokenize/protocol.py
+2
@@ -120,6 +120,8 @@ class TokenizeChatRequest(OpenAIBaseModel):
120
@model_validator(mode="before")
121
@classmethod
122
def check_generation_prompt(cls, data):
123
+ if not isinstance(data, dict):
124
+ return data
125
if data.get("continue_final_message") and data.get("add_generation_prompt"):
126
raise VLLMValidationError(
127
"Cannot set both `continue_final_message` and "
vllm/entrypoints/speech_to_text/transcription/protocol.py
+2
@@ -285,6 +285,8 @@ class TranscriptionRequest(OpenAIBaseModel):
285
@model_validator(mode="before")
286
@classmethod
287
def validate_transcription_request(cls, data):
288
+ if not isinstance(data, dict):
289
+ return data
290
if isinstance(data.get("file"), str):
291
raise HTTPException(
292
status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
vllm/entrypoints/speech_to_text/translation/protocol.py
+2
@@ -271,6 +271,8 @@ class TranslationRequest(OpenAIBaseModel):
271
@model_validator(mode="before")
272
@classmethod
273
def validate_stream_options(cls, data):
274
+ if not isinstance(data, dict):
275
+ return data
276
stream_opts = ["stream_include_usage", "stream_continuous_usage_stats"]
277
stream = data.get("stream", False)
278
if any(bool(data.get(so, False)) for so in stream_opts) and not stream: