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)