fix(SERVE-UTILITY-ENDPOINTS): a non-object JSON body to /tokenize throws through httplib and returns HTTP 500
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 423
- Forks
- 53
- Avg merge
- 20h 26m
- Merged PRs (30d)
- 310
Description
Row: SERVE-UTILITY-ENDPOINTS (.agents/engine-matrix.md:218 — "Tokenize, detokenize, ready, ping, server info, prefix reset")
Found by a fresh review of #2704 (wave PORTQ-5, #2679) re-deriving PORT-NOW entry [178], upstream a6a2a93f9b vllm#52528. Nothing was executed — this is a static reading of source.
This issue exists because PORTQ-5 got entry [178] wrong. It was published as ALREADY_SATISFIED on the premise that "the port reads bodies only through nlohmann::json::find, which returns end() for every non-object". That premise is false for exactly one handler, and it is the one upstream's own test names.
The defect: HTTP 500 on a production route
src/vllm/entrypoints/openai/api_server.cpp:810, inside handle_tokenize:
const bool return_token_strs = body.value("return_token_strs", false);
nlohmann::basic_json::value() (third_party/nlohmann/json.hpp:22267-22283) is not find():
if (JSON_HEDLEY_LIKELY(is_object())) { ... }
JSON_THROW(type_error::create(306, detail::concat("cannot use value() with ", type_name()), this));
It throws type_error 306 for every non-object body — null, [], "str", 3. There is no is_object() guard anywhere between handle_tokenize (:791) and :810; the JSON is parsed at :800-805 (which catches only parse errors) and the tokenizer null-check at :806 is the only thing in between.
The escape path is complete:
/tokenizeis a registered production route —api_server.cpp:1292-1296,server.Post("/tokenize", ...)whose lambda callshandle_tokenize(req.body)outside any try.set_exception_handleris called nowhere in this tree (grep -rn 'set_exception_handler' src include tests examples→ rc 1, no hits; positive control through the identical form and scope,set_payload_max_length→examples/video_studio/main.cpp:373, rc 0).- So
third_party/httplib/httplib.h:12581-12589takes theelsebranch:res.status = StatusCode::InternalServerError_500;.
A malformed-but-valid JSON body therefore returns 500, where upstream returns a 4xx validation error. That is precisely what a6a2a93f9b fixes, and TokenizeChatRequest is in that commit's own parametrised test list (git show a6a2a93f9b — it edits vllm/entrypoints/serve/tokenize/protocol.py and imports TokenizeChatRequest into test_non_object_body_validation.py).
Why this is an oversight and not a deliberate boundary
The rest of the surface already does the right thing three different ways:
- Explicit guards:
src/vllm/entrypoints/openai/speech_api.cpp:96andsrc/vllm/entrypoints/openai/video_api.cpp:158both carryVT_CHECK(json.is_object(), "... body must be a JSON object"). - The embeddings handler guards it directly:
api_server.cpp:526,if (!body.is_object()). - Completions and chat route through
from_jsoninside try/catch and answer 400. - The adjacent
handle_detokenizeis safe by accident: it reaches the body throughcontains()andat(), andcontains()returns false for a non-object rather than throwing, so it answers 400 cleanly.
/tokenize is the one handler that reaches the raw body through value().
Nothing gates it
tests/vllm/entrypoints/openai/test_api_server.cpp carries 15 handle_tokenize call sites (:1005, :1010, :1134, :1432, :1457, :1465, :1472, :1503, :1518, :1532, :1543, :1550, …). Every one posts an object — {}, {"prompt":...}, {"messages":[]}, or .dump() of an object. {} at :1472 is an empty object and passes is_object(). None posts null, [], a bare string or a number.
Size
~5 lines of product code: an is_object() guard after the parse in handle_tokenize returning MakeError(400, "BadRequestError", ...), matching the message shape already used at api_server.cpp:526. Plus a red-first test posting each of null, [], "str" and 3 and asserting 400 rather than a throw.
Consider auditing the remaining handlers for value() on a raw body in the same change; this one was found by reading, not by a checker, and no gate would catch the next one.
Not established
Whether any deployed client sends a non-object body to /tokenize. Nothing was run — the 500 is derived from the source chain above, not observed.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/vllm/entrypoints/openai/api_server.cpp at handle_tokenize and the /tokenize registration, then inspect the existing validation at the embeddings handler. Add regression coverage in tests/vllm/entrypoints/openai/test_api_server.cpp for null, array, string, and numeric bodies. Done means each invalid body returns HTTP 400 instead of propagating to httplib's HTTP 500 path.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- api, backend, testing
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 84/100