Sentry MAILSPRING-CLIENT-2Z: 17 users hit "Cannot read properties of
undefined (reading 'key')" in slate-react's Void.renderText whenever the
composer rendered a void inline node with zero children — its
node.getFirstText() returns undefined, and reading .key on it throws.
convertFromHTML() already patched deserialized JSON so block elements
always get a placeholder empty text child before Value.fromJSON runs
(since that conversion happens outside the schema-aware editor, so
Slate's own void-node normalization never gets a chance to run), but the
same fix was never applied to inline elements. Inline `<img src="cid:...">`
tags (common in signatures/forwards) deserialize to a void inline node
with `nodes: []`, and emoji/template-variable inlines deserialize with no
`nodes` key at all — both slipped through unfixed and crashed on render.
Extend the guard to inline nodes as well, and normalize based on
node.object instead of presence of a `nodes` key so nodes that omit
`nodes` entirely get the same treatment.
Sentry issue
Fixes MAILSPRING-CLIENT-2Z —
Error: Cannot read properties of undefined (reading 'key'), culpritVoid.renderText (slate-react). 17 users impacted, recurring steadily across releases 1.21.1 and 1.22.0.What I observed
Every event in this group has the identical stack:
Looking at the
slate-reactfork's source (bengotow/slate#0.45.1-react),Void.renderTextdoes:If a void node (image/emoji/template-variable — anything marked
isVoid: trueinapp/src/components/composer-editor/conversion.tsx) has zero child nodes,getFirstText()returnsundefinedandchild.keythrows exactly this error.Root cause
convertFromHTML()inapp/src/components/composer-editor/conversion.tsxconverts a draft/message's raw HTML (used for the composer body, including quoted/forwarded content) into a Slate JSON tree and callsValue.fromJSON(json)directly — bypassing the schema-aware editor, so Slate's own "void nodes need a text child" normalization never runs. The code already had a manual workaround for this, but only for block elements:Two deserialize rules produce void inline nodes that fall through this gap:
inline-attachment-plugins.tsx:'<img src="cid:...">'(common in signatures/forwards) deserializes to{ object: 'inline', type: 'image', nodes: [] }.emoji-plugins.tsx/template-plugins.tsx: deserialize to{ object: 'inline', ... }with nonodeskey at all.The pre-existing guard
if (!('nodes' in node)) return;also meant the second case (nonodeskey) skipped the fix function entirely, before it ever reached the empty-children check.So: reply to or forward an email containing an inline
cid:image (or open a draft with an emoji/template-variable inline that lost its text child), and the composer renders a void inline node with no text descendant → crash.Fix
In
optimizeTextNodesForNormalization:node.object === 'text'instead of the presence of anodeskey, and default a missingnodesarray to[]— so inline nodes that omitnodesentirely are treated the same as ones withnodes: [].inlineelements as well asblockelements.Verified locally that both shapes (
nodes: []andnodesomitted) end up with the required empty text child after normalization.Generated by Claude Code