mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-24 14:23:59 -05:00
fix: escape line separators in orjson output (#27819)
orjson emits U+2028, U+2029 and U+0085 raw, where stdlib `json.dumps` escapes them under its default `ensure_ascii=True`. Python treats all three as line boundaries, so with `ENABLE_ORJSON` set, one of them inside model output splits a `data: {...}` SSE frame in half. Both halves then fail to parse and the delta is dropped with no error.
`utils/middleware.py` reassembles frames with `splitlines()`, so an affected response silently loses content on the direct API path. External clients are exposed as well: httpx's `LineDecoder` reimplements the same line-boundary semantics, so any SDK reading the OpenAI-compatible stream through `aiter_lines` breaks on a raw separator.
The three characters are escaped on the way out of `ORJSONCodec.dumps`. That restores parity with stdlib and fixes every reader at once, rather than patching one consumer and leaving external clients broken. They are the complete set: of the ten code points `splitlines()` treats as boundaries, the other seven are below U+0020, where JSON already forces an escape.
The membership guard is load bearing. Calling `translate` unconditionally costs roughly 1.5 us on a typical SSE chunk against 0.115 us for the serialization it wraps, so it would spend more than orjson saves. The three scans cost about 0.04 us.
Payloads containing none of the three are returned unchanged, byte for byte. With `ENABLE_ORJSON` unset, which is the default, none of this code runs.
U+2028 and U+2029 are common in text extracted from PDFs and word processor documents, so the realistic trigger is a model quoting an uploaded file back to the user.
This commit is contained in:
@@ -17,6 +17,11 @@ from open_webui.env import ENABLE_ORJSON
|
||||
if ENABLE_ORJSON:
|
||||
import orjson
|
||||
|
||||
# stdlib escapes these, orjson emits them raw, and Python treats all three as
|
||||
# line boundaries: one raw separator splits an SSE frame that a reader
|
||||
# reassembles with ``splitlines()``.
|
||||
LINE_SEPARATOR_ESCAPES = str.maketrans({'\u2028': '\\u2028', '\u2029': '\\u2029', '\x85': '\\u0085'})
|
||||
|
||||
class ORJSONCodec:
|
||||
"""stdlib-``json``-compatible codec backed by orjson.
|
||||
|
||||
@@ -30,9 +35,12 @@ if ENABLE_ORJSON:
|
||||
@staticmethod
|
||||
def dumps(obj, *args, **kwargs):
|
||||
try:
|
||||
return orjson.dumps(obj).decode('utf-8')
|
||||
serialized = orjson.dumps(obj).decode('utf-8')
|
||||
except (TypeError, ValueError):
|
||||
return engineio_json.dumps(obj, *args, **kwargs)
|
||||
if '\u2028' in serialized or '\u2029' in serialized or '\x85' in serialized:
|
||||
return serialized.translate(LINE_SEPARATOR_ESCAPES)
|
||||
return serialized
|
||||
|
||||
@staticmethod
|
||||
def loads(s, *args, **kwargs):
|
||||
|
||||
Reference in New Issue
Block a user