2026-07-27 09:45:37 +02:00
|
|
|
"""The app-wide JSON codec, selected by the ``ENABLE_ORJSON`` env var.
|
|
|
|
|
|
|
|
|
|
Every module that would otherwise reach for stdlib ``json`` imports ``JSONCodec``
|
|
|
|
|
from here, so the whole app switches implementation from a single flag. With the
|
|
|
|
|
flag off these are stdlib ``json`` and engineio's codec verbatim, so the default
|
|
|
|
|
behaviour is exactly what it was before orjson entered the picture.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import json as stdlib_json
|
|
|
|
|
|
|
|
|
|
from engineio import json as engineio_json
|
|
|
|
|
from open_webui.env import ENABLE_ORJSON
|
|
|
|
|
|
|
|
|
|
if ENABLE_ORJSON:
|
|
|
|
|
import orjson
|
|
|
|
|
|
2026-08-01 03:39:10 +02:00
|
|
|
# orjson emits these raw and Python treats all three as line boundaries: one raw
|
|
|
|
|
# separator splits an SSE frame reassembled with ``splitlines()``. Escaped even
|
|
|
|
|
# where stdlib would not.
|
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.
2026-07-31 23:25:30 +02:00
|
|
|
LINE_SEPARATOR_ESCAPES = str.maketrans({'\u2028': '\\u2028', '\u2029': '\\u2029', '\x85': '\\u0085'})
|
|
|
|
|
|
2026-08-01 03:39:10 +02:00
|
|
|
# Module-level because CPython rebuilds these dicts on every call.
|
|
|
|
|
FAST_PATH_KWARGS = ({'separators': (',', ':')}, {'ensure_ascii': False})
|
|
|
|
|
|
2026-07-27 09:45:37 +02:00
|
|
|
class ORJSONCodec:
|
|
|
|
|
"""stdlib-``json``-compatible codec backed by orjson.
|
|
|
|
|
|
2026-08-01 03:39:10 +02:00
|
|
|
The fast path is not byte-for-byte stdlib: it is always compact, formats
|
|
|
|
|
floats orjson's way (``1e16``, not ``1e+16``), and is raw UTF-8 apart from
|
|
|
|
|
the three line separators escaped above, so a ``separators`` caller loses
|
|
|
|
|
stdlib's ASCII escaping and an ``ensure_ascii=False`` caller loses its
|
|
|
|
|
spacing. ``dumps`` also serializes ``datetime``/``UUID``/dataclasses that
|
|
|
|
|
stdlib refuses, and encodes ``NaN``/``Infinity`` as ``null``. ``loads``
|
|
|
|
|
decodes integers above ``2**64-1`` or below ``-2**63`` as ``float`` and does
|
|
|
|
|
not enforce engineio's 100-digit integer-literal limit.
|
|
|
|
|
|
|
|
|
|
What orjson does reject (non-str dict keys and oversized ints on ``dumps``,
|
|
|
|
|
the ``NaN``/``Infinity`` literals on ``loads``) falls back to engineio's
|
|
|
|
|
stdlib-based codec, and with it stdlib's formatting.
|
2026-07-27 09:45:37 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
JSONDecodeError = engineio_json.JSONDecodeError
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def dumps(obj, *args, **kwargs):
|
2026-08-01 03:39:10 +02:00
|
|
|
if args or (kwargs and kwargs not in FAST_PATH_KWARGS):
|
2026-07-31 17:45:07 -04:00
|
|
|
return engineio_json.dumps(obj, *args, **kwargs)
|
2026-07-27 09:45:37 +02:00
|
|
|
try:
|
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.
2026-07-31 23:25:30 +02:00
|
|
|
serialized = orjson.dumps(obj).decode('utf-8')
|
2026-07-27 09:45:37 +02:00
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
return engineio_json.dumps(obj, *args, **kwargs)
|
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.
2026-07-31 23:25:30 +02:00
|
|
|
if '\u2028' in serialized or '\u2029' in serialized or '\x85' in serialized:
|
|
|
|
|
return serialized.translate(LINE_SEPARATOR_ESCAPES)
|
|
|
|
|
return serialized
|
2026-07-27 09:45:37 +02:00
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def loads(s, *args, **kwargs):
|
2026-07-31 17:45:07 -04:00
|
|
|
if args or kwargs:
|
|
|
|
|
return engineio_json.loads(s, *args, **kwargs)
|
2026-07-27 09:45:37 +02:00
|
|
|
try:
|
|
|
|
|
return orjson.loads(s)
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
return engineio_json.loads(s, *args, **kwargs)
|
|
|
|
|
|
|
|
|
|
# Drop-in for stdlib ``json``: ``JSONCodec.dumps`` / ``JSONCodec.loads``.
|
|
|
|
|
JSONCodec = ORJSONCodec
|
|
|
|
|
# Codec handed to the socket.io/engineio managers, which default to their own.
|
|
|
|
|
SOCKETIO_JSON = ORJSONCodec
|
|
|
|
|
else:
|
|
|
|
|
JSONCodec = stdlib_json
|
|
|
|
|
SOCKETIO_JSON = engineio_json
|