[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: