From ca155b8bbd3581affbe1d9e325650a50fd043550 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 14:22:12 +0000 Subject: [PATCH] Prevent composer crash from Slate node-key errors when breaking out of nested quotes or pasting rich content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slate's own compound change functions (unwrapBlockAtRange, insertFragmentAtRange in the vendored slate/lib/slate.js) collect node keys before performing several sequential move/remove operations, and those keys can go stale partway through the same operation for certain nested-blockquote or multi-block-paste shapes. When that happens, Slate's internal moveNodeByKey/assertPath throws "could not find node with path or key", which was previously uncaught and crashed/aborted the composer mid-mutation (MAILSPRING-CLIENT-2X, 50 users impacted). Wrap the two call sites that trigger this (breaking out of a nested blockquote via Enter or the toolbar button, and pasting HTML into the composer) in try/catch so the failure is logged and the command safely no-ops instead of throwing an uncaught exception. Because Slate only commits a value and fires onChange once a top-level command completes successfully, a mid-command throw never reaches the persisted draft — so the safe fallback is simply to stop. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01RLUjkxhpBL8yT4tTHrhyQK --- .../composer-editor/base-block-plugins.tsx | 19 +++++++++++++++++-- .../composer-editor/composer-editor.tsx | 15 ++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/app/src/components/composer-editor/base-block-plugins.tsx b/app/src/components/composer-editor/base-block-plugins.tsx index 0f16d87ebe..6e8c5eeacd 100644 --- a/app/src/components/composer-editor/base-block-plugins.tsx +++ b/app/src/components/composer-editor/base-block-plugins.tsx @@ -51,8 +51,23 @@ function toggleBlockTypeWithBreakout(editor: Editor, type: string) { if (idx !== -1) { const depth = ancestors.size - idx; if (depth > 0) { - editor.splitBlock(ancestors.size - idx); - for (let x = 0; x < depth; x++) editor.unwrapBlock({ type }); + try { + editor.splitBlock(ancestors.size - idx); + for (let x = 0; x < depth; x++) editor.unwrapBlock({ type }); + } catch (err) { + // Slate's `unwrapBlock` (unwrapBlockAtRange in slate/lib/slate.js) walks the + // ancestor's children and moves them out one at a time using node keys it + // collected before the split/unwind began. When breaking out of multiple levels + // of nested blocks in one go (eg. multi-level quoted replies), those keys can + // point at nodes that a prior split/unwrap already relocated or removed, and + // Slate's own `moveNodeByKey`/`assertPath` throws "could not find node with path + // or key" instead of recovering. This is a bug inside Slate's compound change + // function, not something we can fix from a plugin, so we just stop here rather + // than let the exception escape and abort the whole editor command mid-mutation. + // See MAILSPRING-CLIENT-2X. + console.warn('toggleBlockTypeWithBreakout: failed to break out of nested block', err); + return; + } } editor.setBlocks(BLOCK_CONFIG.div.type); } else { diff --git a/app/src/components/composer-editor/composer-editor.tsx b/app/src/components/composer-editor/composer-editor.tsx index 18b44fded7..805e2fa0bb 100644 --- a/app/src/components/composer-editor/composer-editor.tsx +++ b/app/src/components/composer-editor/composer-editor.tsx @@ -294,7 +294,20 @@ export class ComposerEditor extends React.Component