mudler / mudler/vllm.cpp

fix(SERVE-UTILITY-ENDPOINTS): a non-object JSON body to /tokenize throws through httplib and returns HTTP 500

Open
#2,707 0 comments 0 reactions 0 assignees View on GitHub

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:

  1. /tokenize is a registered production route — api_server.cpp:1292-1296, server.Post("/tokenize", ...) whose lambda calls handle_tokenize(req.body) outside any try.
  2. set_exception_handler is 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_lengthexamples/video_studio/main.cpp:373, rc 0).
  3. So third_party/httplib/httplib.h:12581-12589 takes the else branch: 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:96 and src/vllm/entrypoints/openai/video_api.cpp:158 both carry VT_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_json inside try/catch and answer 400.
  • The adjacent handle_detokenize is safe by accident: it reaches the body through contains() and at(), and contains() 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.