Skip to content

Fix composer crash on quoted emails with inline images (Void.renderText) - #2776

Merged
bengotow merged 1 commit into
masterfrom
claude/awesome-ritchie-jw81hp
Jul 18, 2026
Merged

Fix composer crash on quoted emails with inline images (Void.renderText)#2776
bengotow merged 1 commit into
masterfrom
claude/awesome-ritchie-jw81hp

Conversation

@bengotow

Copy link
Copy Markdown
Collaborator

Sentry issue

Fixes MAILSPRING-CLIENT-2ZError: Cannot read properties of undefined (reading 'key'), culprit Void.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:

react-dom ... 
slate-react.js (Void.render)
slate-react.js (Void.renderText)

Looking at the slate-react fork's source (bengotow/slate#0.45.1-react), Void.renderText does:

this.renderText = function () {
  var child = node.getFirstText();
  return React.createElement(Text, { ..., key: child.key, node: child, ... });
};

If a void node (image/emoji/template-variable — anything marked isVoid: true in app/src/components/composer-editor/conversion.tsx) has zero child nodes, getFirstText() returns undefined and child.key throws exactly this error.

Root cause

convertFromHTML() in app/src/components/composer-editor/conversion.tsx converts a draft/message's raw HTML (used for the composer body, including quoted/forwarded content) into a Slate JSON tree and calls Value.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:

if (node.object === 'block' && node.nodes.length === 0) { /* add empty text child */ }

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 no nodes key at all.

The pre-existing guard if (!('nodes' in node)) return; also meant the second case (no nodes key) 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:

  • Bail out based on node.object === 'text' instead of the presence of a nodes key, and default a missing nodes array to [] — so inline nodes that omit nodes entirely are treated the same as ones with nodes: [].
  • Extend the "ensure an empty text child" fix to inline elements as well as block elements.

Verified locally that both shapes (nodes: [] and nodes omitted) end up with the required empty text child after normalization.


Generated by Claude Code

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.
@indent-staging

indent-staging Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor
PR Summary

Fixes Sentry MAILSPRING-CLIENT-2Z (Cannot read properties of undefined (reading 'key') in slate-react's Void.renderText) that hit 17 users when the composer opened quoted emails containing void inline nodes with no text child. convertFromHTML's post-deserialization normalization already injected an empty text-node child into empty block nodes so Value.fromJSON wouldn't produce a void node that violates Slate's "every void has a text descendant" invariant; this PR extends the same fix to inline nodes and also handles inlines that omit the nodes key entirely.

  • In app/src/components/composer-editor/conversion.tsx, replace the optimizeTextNodesForNormalization early-return guard so it explicitly short-circuits on text nodes and normalizes missing/nullish nodes to [] on everything else.
  • Broaden the empty-text-child injection from object === 'block' to object === 'block' || object === 'inline', so void inline cid: images (deserialized with nodes: []) and emoji/template-variable inlines (deserialized without a nodes key) both get a placeholder text descendant before Value.fromJSON runs.

Issues

No issues found.

CI Checks

All CI checks passed on d9b43c7.

Custom Rules 3 rules evaluated, 3 passed, 0 failed

Passing This is a longer title to see what happens when they are too long to fit
Passing B
Passing Ben Rule

View all rules

@indent

indent Bot commented Jul 17, 2026

Copy link
Copy Markdown
PR Summary

Fixes the Sentry crash Void.renderTextCannot read properties of undefined (reading 'key') (MAILSPRING-CLIENT-2Z) that occurred when the composer rendered quoted/forwarded emails containing inline cid: images.

The root cause is that convertFromHTML() builds a Slate JSON tree and calls Value.fromJSON() directly, bypassing the schema normalization that gives void nodes a required empty text child. The existing manual workaround only handled empty block nodes, so void inline nodes were left with zero text descendants and getFirstText().key threw at render time. This PR extends the workaround to inline nodes.

  • Early-return guard changed to skip only text nodes (equivalent to the old 'nodes' in node check for text).
  • Inline nodes deserialized without a nodes array (emoji, template variables) are normalized to an empty array so the fix applies uniformly.
  • The empty-text-child insertion now applies to inline nodes as well as block nodes, covering all void inlines (cid: images with nodes: [], emoji, template variables).

Issues

Review closed.

CI Checks

All CI checks passed. The test check succeeded at commit d9b43c7.

@bengotow

Copy link
Copy Markdown
Collaborator Author

This looks good to me but definitely requires manual testing.

@bengotow
bengotow merged commit 79adca6 into master Jul 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants