fix(serve): an unpaired surrogate escape answered 500 "engine failed" - #1589
Merged
Merged
Conversation
JSON can spell a lone UTF-16 surrogate ("\ud83d"), which is what a
client produces when it cuts a string between the two halves of an
emoji. json.loads accepts it, but no UTF-8 can carry it, and the first
thing Engine.generate does is prompt.encode("utf-8"). The
UnicodeEncodeError reached do_POST's catch-all, so /v1/chat/completions,
/v1/completions and /v1/messages answered HTTP 500 "The colibri engine
failed to process the request.". Undecodable UTF-8 in the raw body is
already a 400 in read_json; this is the same invalid text, escaped.
read_json now also rejects a body whose strings cannot be encoded as
UTF-8, with a 400 that says why. Paired surrogate escapes (an escaped
emoji) are unaffected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
A request with a JSON string that holds an unpaired UTF-16 surrogate escape gets HTTP 500 and a message that blames the engine. The escape looks like
"\ud83d": a client produces it when it cuts a string between the two halves of an emoji (a JSsliceon a long message is the usual way). It fails on/v1/chat/completions,/v1/completionsand/v1/messages.Against a real
coli serve(tiny Qwen3.6 fixture, unfixeddev):The engine is fine; the request is invalid. OpenAI SDKs retry a 5xx, so the client also resends it before surfacing an error that points at the wrong side.
Root cause
json.loadsaccepts a lone surrogate escape, soread_jsonreturns a body containing text that no UTF-8 byte sequence can represent. The first step ofEngine.generateispayload = prompt.encode("utf-8"). TheUnicodeEncodeErrorreachesdo_POST's catch-all, which answers 500engine_error.read_jsonalready turns undecodable UTF-8 in the raw body into a 400 ("Request body must be valid JSON."). An escaped lone surrogate is the same invalid text, spelled differently.generatealso already refuses the neighbouring case, NUL bytes in a prompt, with a 400.Fix
In
read_json, after parsing, check that the body can be encoded as UTF-8 (json.dumps(body, ensure_ascii=False).encode("utf-8")). If it cannot, answer 400 with "Request body contains an unpaired UTF-16 surrogate escape; strings must be valid Unicode." The check runs once per request, and a body is at mostMAX_BODY= 4 MiB.Because this is at the request boundary, it covers every endpoint and field: prompts, messages, tool definitions, GBNF grammars, and the cluster registry, which shares
read_json. Paired escapes such asπdecode to a real character and are unaffected.Tests
New
HTTPTest.test_unpaired_surrogate_is_a_client_errorposts a lone\ud83dto all three endpoints and expects 400.FakeEnginenever encodes the prompt, so the test wraps itsgeneratewith the same first step the realEngine.generatetakes (prompt.encode("utf-8")). Without the fix, the server answers exactly the production 500.Fail-before on unfixed
dev:Verification
openai_serverorcluster(20 modules;test_fp8_repack_full_familyleft out because it needs torch here) passes withPYTHONUTF8=1: OK, 28 skipped. In the cp949 locale the only errors are the twotest_openai_tools_v41_e2ecases that read their fixture with a locale-encodedread_text(). They error the same way on unfixeddev.coli serveas above, after the fix:request failedline.coli chat --attach. On a Windows console whose code page is not UTF-8, piped UTF-8 input with an emoji reaches the server as lone surrogates. Before the fix that turn failed with a 500; now the chat prints the 400 and its message.π€ Generated with Claude Code