Fix chat completion 500 on non-object JSON bodies (#51654)
Signed-off-by: Tarun Kumar <takumar@redhat.com>
Tarun Kumar committed
Aug 11, 2026 at 07:01 UTC
48bada6ea49ad7f3ecbe03128aa76562089c8b00
2 files changed
+58
-7
tests/entrypoints/openai/chat_completion/test_non_object_body_validation.py
new
+38
@@ -0,0 +1,38 @@
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
+
10
+import pytest
11
+from pydantic import ValidationError
12
+
13
+from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
14
+
15
+
16
+@pytest.mark.parametrize(
17
+ "payload",
18
+ [
19
+ "this is not valid json{{{",
20
+ ["not", "an", "object"],
21
+ 42,
22
+ None,
23
+ ],
24
+)
25
+def test_chat_completion_request_rejects_non_object_body(payload):
26
+ with pytest.raises(ValidationError):
27
+ ChatCompletionRequest.model_validate(payload)
28
+
29
+
30
+def test_chat_completion_request_cache_salt_still_validated_on_dict():
31
+ with pytest.raises(ValidationError, match="cache_salt"):
32
+ ChatCompletionRequest.model_validate(
33
+ {
34
+ "model": "qwen",
35
+ "messages": [{"role": "user", "content": "hello"}],
36
+ "cache_salt": "",
37
+ }
38
+ )
vllm/entrypoints/openai/chat_completion/protocol.py
+20
-7
@@ -745,6 +745,8 @@ class ChatCompletionRequest(OpenAIBaseModel):
745
@model_validator(mode="before")
746
@classmethod
747
def validate_response_format(cls, data):
748
+ if not isinstance(data, dict):
749
+ return data
750
response_format = data.get("response_format")
751
if response_format is None:
752
return data
@@ -776,6 +778,8 @@ class ChatCompletionRequest(OpenAIBaseModel):
778
@model_validator(mode="before")
779
@classmethod
780
def validate_stream_options(cls, data):
781
+ if not isinstance(data, dict):
782
+ return data
783
if data.get("stream_options") and not data.get("stream"):
784
raise VLLMValidationError(
785
"Stream options can only be defined when `stream=True`.",
@@ -787,6 +791,8 @@ class ChatCompletionRequest(OpenAIBaseModel):
791
@model_validator(mode="before")
792
@classmethod
793
def check_logprobs(cls, data):
794
+ if not isinstance(data, dict):
795
+ return data
796
if data.get("logprob_token_ids") and data.get("use_beam_search"):
797
raise VLLMValidationError(
798
"`logprob_token_ids` is not supported with beam search.",
@@ -845,6 +851,8 @@ class ChatCompletionRequest(OpenAIBaseModel):
851
def check_structured_outputs_count(cls, data):
852
if isinstance(data, ValueError):
853
raise data
854
+ if not isinstance(data, dict):
855
+ return data
856
857
if data.get("structured_outputs", None) is None:
858
return data
@@ -970,6 +978,8 @@ class ChatCompletionRequest(OpenAIBaseModel):
978
@model_validator(mode="before")
979
@classmethod
980
def check_generation_prompt(cls, data):
981
+ if not isinstance(data, dict):
982
+ return data
983
if data.get("continue_final_message") and data.get("add_generation_prompt"):
984
raise VLLMValidationError(
985
"Cannot set both `continue_final_message` and "
@@ -1105,6 +1115,8 @@ class BatchChatCompletionRequest(OpenAIBaseModel):
1115
def check_batch_mode(cls, data: Any) -> Any:
1116
if isinstance(data, BatchChatCompletionRequest):
1117
data = data.model_dump(exclude_unset=True)
1118
+ if not isinstance(data, dict):
1119
+ return data
1120
if data.get("use_beam_search"):
1121
raise VLLMValidationError(
1122
"Batch chat completions do not support beam search. "
@@ -1117,13 +1129,14 @@ class BatchChatCompletionRequest(OpenAIBaseModel):
1129
parameter="logprob_token_ids",
1130
)
1131
response_format = data.get("response_format")
1120
- rf_type = (
1121
- response_format.get("type")
1122
- if isinstance(response_format, dict)
1123
- else getattr(response_format, "type", None)
1124
- )
1125
- if rf_type == "structural_tag":
1126
- validate_structural_tag_response_format(response_format)
1132
+ if response_format is not None:
1133
+ rf_type = (
1134
+ response_format.get("type")
1135
+ if isinstance(response_format, dict)
1136
+ else getattr(response_format, "type", None)
1137
+ )
1138
+ if rf_type == "structural_tag":
1139
+ validate_structural_tag_response_format(response_format)
1140
if (structured_outputs := data.get("structured_outputs")) is not None:
1141
validate_structured_outputs_structural_tag(structured_outputs)
1142
n = data.get("n", 1)