Skip to content

Commit e2c57db

Browse files
committed
fix(realtime): measure presence field cap in utf-8 bytes
The cap compared UTF-16 code units against a byte budget, so a multi-byte payload could pass the check and still land several times larger in the room hash. It now measures the UTF-8 bytes Redis actually stores. Raises the ceiling to 16384. A table cell selection carries four ids capped at 200 characters each, and multi-byte characters plus JSON escaping can expand a legitimate worst case to roughly 5 KB - above the previous 4096, so the old bound could have dropped real presence.
1 parent cf80252 commit e2c57db

1 file changed

Lines changed: 15 additions & 13 deletions

File tree

‎apps/realtime/src/rooms/redis-manager.ts‎

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -123,18 +123,23 @@ return 1
123123
`
124124

125125
/**
126-
* Ceiling on the JSON length of a single presence field. Every legitimate payload is far
127-
* below this — a cursor is tens of characters, and the largest handler-bounded selection
128-
* (two cell refs whose ids cap at 200) stays under 500 — so this never trims real presence.
129-
* It is a backstop for presence-bearing events whose handler validation is missing or
130-
* regresses, capping what one socket can park in the shared room hash and fan out to peers.
126+
* Ceiling on a single presence field, measured in the UTF-8 bytes Redis actually stores
127+
* rather than UTF-16 code units, so a multi-byte payload can't pass a character-based check
128+
* and still land several times larger in the room hash.
129+
*
130+
* The largest legitimate payload is a table cell selection: four ids capped at 200
131+
* characters each. Multi-byte characters and JSON escaping can expand those well past
132+
* their character count, so the realistic worst case approaches 5 KB — this sits comfortably
133+
* above that, and a backstop that could trim real presence would be worse than a loose one.
134+
* It bounds what one socket can park in the shared room hash and fan out to every peer when
135+
* a presence-bearing event's handler validation is missing or regresses.
131136
*/
132-
const MAX_PRESENCE_FIELD_LENGTH = 4096
137+
const MAX_PRESENCE_FIELD_BYTES = 16384
133138

134139
/**
135140
* Serialize one presence field for the activity script. Returns `''` when there is no
136141
* update (the script skips the field) and, defensively, when the value exceeds
137-
* {@link MAX_PRESENCE_FIELD_LENGTH} — dropping just that field rather than the whole
142+
* {@link MAX_PRESENCE_FIELD_BYTES} — dropping just that field rather than the whole
138143
* update, so a single oversized field can't suppress the others or the activity refresh.
139144
*/
140145
function serializePresenceField(
@@ -144,12 +149,9 @@ function serializePresenceField(
144149
): string {
145150
if (value === undefined) return ''
146151
const serialized = JSON.stringify(value)
147-
if (serialized.length > MAX_PRESENCE_FIELD_LENGTH) {
148-
logger.warn('Dropping oversized presence field', {
149-
field,
150-
socketId,
151-
length: serialized.length,
152-
})
152+
const bytes = Buffer.byteLength(serialized, 'utf8')
153+
if (bytes > MAX_PRESENCE_FIELD_BYTES) {
154+
logger.warn('Dropping oversized presence field', { field, socketId, bytes })
153155
return ''
154156
}
155157
return serialized

0 commit comments

Comments
 (0)