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
|
perf: write task payloads to Redis as bytes (#28833)
Saving a streaming response serialized the payload with orjson, decoded it to
str, scanned it for the three Unicode line separators and let redis-py encode
it straight back to UTF-8: on an 8 MB non-ASCII chat that is 6.9 ms and ~22 MB
of transient buffers per write, synchronously on the event loop.
json_codec now exposes dumps_bytes, which returns the serialized payload as
UTF-8 bytes without the line-separator escaping, and the two Redis writes in
tasks.py use it. That escaping only protects line-framed protocols such as
SSE; every reader of these Redis values re-parses them before anything is
served, and the escaped and raw forms parse identically, so mixed versions
during a rolling deploy interoperate both ways. The same write drops to
0.9 ms and one 8 MB buffer (7.5x), with 31-66% saved on KB-sized writes.
With ENABLE_ORJSON off, dumps_bytes wraps stdlib json, behaviour unchanged.
The str path keeps the escaping but applies it with chained str.replace
instead of a translate table, cutting a separator-containing 8 MB payload
from 312 ms to 5.7 ms with byte-identical output.
2026-08-20 21:58:52 +02:00
|
|
|
behaviour is exactly what it was before orjson entered the picture. ``dumps_bytes``
|
|
|
|
|
returns UTF-8 bytes for sinks that re-parse the payload; under orjson it skips
|
|
|
|
|
both the str round trip and the line-separator escaping ``dumps`` applies, so
|
|
|
|
|
never feed it to line-framed output such as SSE.
|
2026-07-27 09:45:37 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
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
|
|
|
# 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
|
perf: write task payloads to Redis as bytes (#28833)
Saving a streaming response serialized the payload with orjson, decoded it to
str, scanned it for the three Unicode line separators and let redis-py encode
it straight back to UTF-8: on an 8 MB non-ASCII chat that is 6.9 ms and ~22 MB
of transient buffers per write, synchronously on the event loop.
json_codec now exposes dumps_bytes, which returns the serialized payload as
UTF-8 bytes without the line-separator escaping, and the two Redis writes in
tasks.py use it. That escaping only protects line-framed protocols such as
SSE; every reader of these Redis values re-parses them before anything is
served, and the escaped and raw forms parse identically, so mixed versions
during a rolling deploy interoperate both ways. The same write drops to
0.9 ms and one 8 MB buffer (7.5x), with 31-66% saved on KB-sized writes.
With ENABLE_ORJSON off, dumps_bytes wraps stdlib json, behaviour unchanged.
The str path keeps the escaping but applies it with chained str.replace
instead of a translate table, cutting a separator-containing 8 MB payload
from 312 ms to 5.7 ms with byte-identical output.
2026-08-20 21:58:52 +02:00
|
|
|
the three line separators ``dumps`` escapes, so a ``separators`` caller loses
|
2026-08-01 03:39:10 +02:00
|
|
|
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)
|
perf: write task payloads to Redis as bytes (#28833)
Saving a streaming response serialized the payload with orjson, decoded it to
str, scanned it for the three Unicode line separators and let redis-py encode
it straight back to UTF-8: on an 8 MB non-ASCII chat that is 6.9 ms and ~22 MB
of transient buffers per write, synchronously on the event loop.
json_codec now exposes dumps_bytes, which returns the serialized payload as
UTF-8 bytes without the line-separator escaping, and the two Redis writes in
tasks.py use it. That escaping only protects line-framed protocols such as
SSE; every reader of these Redis values re-parses them before anything is
served, and the escaped and raw forms parse identically, so mixed versions
during a rolling deploy interoperate both ways. The same write drops to
0.9 ms and one 8 MB buffer (7.5x), with 31-66% saved on KB-sized writes.
With ENABLE_ORJSON off, dumps_bytes wraps stdlib json, behaviour unchanged.
The str path keeps the escaping but applies it with chained str.replace
instead of a translate table, cutting a separator-containing 8 MB payload
from 312 ms to 5.7 ms with byte-identical output.
2026-08-20 21:58:52 +02:00
|
|
|
# Raw, these three split an SSE frame reassembled with ``splitlines()``.
|
|
|
|
|
# A dict-table translate walks char by char; chained replace runs on C fast paths.
|
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:
|
perf: write task payloads to Redis as bytes (#28833)
Saving a streaming response serialized the payload with orjson, decoded it to
str, scanned it for the three Unicode line separators and let redis-py encode
it straight back to UTF-8: on an 8 MB non-ASCII chat that is 6.9 ms and ~22 MB
of transient buffers per write, synchronously on the event loop.
json_codec now exposes dumps_bytes, which returns the serialized payload as
UTF-8 bytes without the line-separator escaping, and the two Redis writes in
tasks.py use it. That escaping only protects line-framed protocols such as
SSE; every reader of these Redis values re-parses them before anything is
served, and the escaped and raw forms parse identically, so mixed versions
during a rolling deploy interoperate both ways. The same write drops to
0.9 ms and one 8 MB buffer (7.5x), with 31-66% saved on KB-sized writes.
With ENABLE_ORJSON off, dumps_bytes wraps stdlib json, behaviour unchanged.
The str path keeps the escaping but applies it with chained str.replace
instead of a translate table, cutting a separator-containing 8 MB payload
from 312 ms to 5.7 ms with byte-identical output.
2026-08-20 21:58:52 +02:00
|
|
|
return serialized.replace('\u2028', '\\u2028').replace('\u2029', '\\u2029').replace('\x85', '\\u0085')
|
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
|
|
|
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
|
perf: write task payloads to Redis as bytes (#28833)
Saving a streaming response serialized the payload with orjson, decoded it to
str, scanned it for the three Unicode line separators and let redis-py encode
it straight back to UTF-8: on an 8 MB non-ASCII chat that is 6.9 ms and ~22 MB
of transient buffers per write, synchronously on the event loop.
json_codec now exposes dumps_bytes, which returns the serialized payload as
UTF-8 bytes without the line-separator escaping, and the two Redis writes in
tasks.py use it. That escaping only protects line-framed protocols such as
SSE; every reader of these Redis values re-parses them before anything is
served, and the escaped and raw forms parse identically, so mixed versions
during a rolling deploy interoperate both ways. The same write drops to
0.9 ms and one 8 MB buffer (7.5x), with 31-66% saved on KB-sized writes.
With ENABLE_ORJSON off, dumps_bytes wraps stdlib json, behaviour unchanged.
The str path keeps the escaping but applies it with chained str.replace
instead of a translate table, cutting a separator-containing 8 MB payload
from 312 ms to 5.7 ms with byte-identical output.
2026-08-20 21:58:52 +02:00
|
|
|
|
|
|
|
|
def dumps_bytes(obj) -> bytes:
|
|
|
|
|
"""JSON as UTF-8 bytes, skipping the str round trip and the escaping ``dumps`` does."""
|
|
|
|
|
try:
|
|
|
|
|
return orjson.dumps(obj)
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
return engineio_json.dumps(obj).encode('utf-8')
|
2026-07-27 09:45:37 +02:00
|
|
|
else:
|
|
|
|
|
JSONCodec = stdlib_json
|
|
|
|
|
SOCKETIO_JSON = engineio_json
|
perf: write task payloads to Redis as bytes (#28833)
Saving a streaming response serialized the payload with orjson, decoded it to
str, scanned it for the three Unicode line separators and let redis-py encode
it straight back to UTF-8: on an 8 MB non-ASCII chat that is 6.9 ms and ~22 MB
of transient buffers per write, synchronously on the event loop.
json_codec now exposes dumps_bytes, which returns the serialized payload as
UTF-8 bytes without the line-separator escaping, and the two Redis writes in
tasks.py use it. That escaping only protects line-framed protocols such as
SSE; every reader of these Redis values re-parses them before anything is
served, and the escaped and raw forms parse identically, so mixed versions
during a rolling deploy interoperate both ways. The same write drops to
0.9 ms and one 8 MB buffer (7.5x), with 31-66% saved on KB-sized writes.
With ENABLE_ORJSON off, dumps_bytes wraps stdlib json, behaviour unchanged.
The str path keeps the escaping but applies it with chained str.replace
instead of a translate table, cutting a separator-containing 8 MB payload
from 312 ms to 5.7 ms with byte-identical output.
2026-08-20 21:58:52 +02:00
|
|
|
|
|
|
|
|
def dumps_bytes(obj) -> bytes:
|
|
|
|
|
"""JSON as UTF-8 bytes; here simply ``dumps`` encoded."""
|
|
|
|
|
return stdlib_json.dumps(obj).encode('utf-8')
|