Dropping files crashes when the editor is torn down or the selection can't accept them#1107
Dropping files crashes when the editor is torn down or the selection can't accept them#1107jorgemanrubia wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR hardens the editor’s file-drop/upload path against teardown and selection edge-cases that were causing production TypeErrors, primarily by adding defensive guards and correcting inserter/gallery fallback behavior.
Changes:
- Make
Contents#uploadFilesa no-op when called on a disposedContents(late drop after teardown). - Ensure gallery uploads fall back to creating a new gallery when
$findOrCreateGalleryForImagereturnsnull. - Prevent crashes in node insertion by handling node selections correctly and guarding
selectEnd()when the caret resolves tonull. - Add unit regression tests for several of the reported crash variants.
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| test/javascript/unit/editor/upload_files.test.js | Adds unit tests covering disposed-contents uploads, gallery fallback behavior, and node-selection insertion. |
| src/editor/contents/uploader.js | Adds a null-safe gallery fallback when the selected/resolved node can’t join a gallery. |
| src/editor/contents/node_inserter.js | Fixes selection handling for shadow roots and guards code insertion selection updates. |
| src/editor/contents.js | Adds a guard to prevent uploadFiles from running after Contents disposal. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| caret.getNodeAtCaret().selectEnd() | ||
| caret.getNodeAtCaret()?.selectEnd() |
There was a problem hiding this comment.
🤖 Added — test/browser/tests/attachments/upload_failure_guards.test.js now covers this path: with the caret in an empty code block, Contents#uploadFiles (the same entry point document-level drop zones use) used to crash with Cannot read properties of null (reading 'selectEnd'). Verified red against main's source and green with the fix.
There was a problem hiding this comment.
I suspect the bug might be on L39.
if (!node.isAttached()) continueIf there's no caret.getNodeAtCaret() then no nodes were inserted into the CodeNode: I'd suspect the filter is removing all the eligible nodes.
| let paragraph | ||
| editorElement.editor.read(() => { paragraph = $getRoot().getFirstChild() }) | ||
|
|
||
| // Selection reports a previewable image but resolves to a node that can't join a gallery | ||
| Object.defineProperty(editorElement.selection, "isOnPreviewableImage", { get: () => true }) | ||
| editorElement.selection.selectedNodeWithOffset = () => ({ node: paragraph, offset: 0 }) |
There was a problem hiding this comment.
🤖 Done — the stub now resolves the node at call time inside the calling update (() => ({ node: $getRoot().getFirstChild(), offset: 0 })) instead of reusing a node captured from an earlier read. This case stays a unit test: the state it exercises (selection whose first node is a previewable image while the anchor resolves to a node that can't join a gallery) comes from stale selection restoration during drag-and-drop races and can't be constructed organically in a browser — Lexical normalizes the anchor of any drivable selection onto the attachment itself. The other regression cases moved to browser tests in test/browser/tests/attachments/upload_failure_guards.test.js.
Dropping files could throw in three ways, all surfaced by Sentry BC3-JS-N7D5 from Basecamp's document-level drop zone: - Contents.uploadFiles ran against a disposed Contents (editorElement nulled after editor teardown), throwing on supportsAttachments. - ShadowRootNodeInserter.handles assumed the selection had an anchor, throwing for node selections (e.g. an attachment selected on drop). - GalleryUploader assumed $findOrCreateGalleryForImage always returned a gallery, throwing on splice when the resolved node couldn't join one. It now falls back to creating a fresh gallery, and CodeNodeInserter tolerates a missing node at the caret after insert.
a3707ef to
0ef6ddc
Compare
samuelpecher
left a comment
There was a problem hiding this comment.
I wonder if the core bug here is upstream in the dropzone implementation rather than here: insertingFiles in a torn-down editor seems like implementation error.
The CodeNodeInserter fix might be in the wrong place
| // $findOrCreateGalleryForImage returns null when the resolved node can't join a gallery | ||
| if (!this.#gallery) { |
There was a problem hiding this comment.
This feels like masking an error within #selectionIsAfterGalleryEdge which calls ImageGalleryNode.canCollapseWith(selection.nodeBeforeCursor). The logic would dictate that the selection.nodeBeforeCursor should be a gallery-collapsible node
| } | ||
|
|
||
| caret.getNodeAtCaret().selectEnd() | ||
| caret.getNodeAtCaret()?.selectEnd() |
There was a problem hiding this comment.
I suspect the bug might be on L39.
if (!node.isAttached()) continueIf there's no caret.getNodeAtCaret() then no nodes were inserted into the CodeNode: I'd suspect the filter is removing all the eligible nodes.
| } | ||
|
|
||
| uploadFiles(files, { selectLast } = {}) { | ||
| if (!this.editorElement) return // Disposed after editor teardown; a late drop can still land here |
There was a problem hiding this comment.
Is this a potentially swallowing an implementation error where we shouldn't be pasting files into a torn-down editor from Dropzone?
Dropping files onto Basecamp pages crashes when the drop lands on an editor that has been torn down, or while the current selection can't accept the insertion. Fixes BC3-JS-N7D5 — 583 events / 533 users in 3 days, all funneling through Basecamp's document-level drop zone into
Contents.uploadFiles. Bug card: 9982374185.The Sentry issue groups four variants of the same drop path:
Cannot read properties of null (reading 'supportsAttachments')—Contents.uploadFilescalled on a disposedContents:dispose()nullseditorElement, but the editor element still exposes the disposed instance viaelement.contents, so a drop landing right after teardown (Turbo morph/navigation) crashes. Now handled deliberately as a no-op.Cannot read properties of undefined (reading 'getNode')—ShadowRootNodeInserter.handlesreadselection?.anchor.getNode(); node selections (e.g. an attachment selected when the drop happens) have noanchor. Fixed the optional chaining soNodeSelectionNodeInsertergets to handle them, as intended.Cannot read properties of null (reading 'splice')—GalleryUploader.#findOrCreateGalleryassumed$findOrCreateGalleryForImagealways returns a gallery, but it returnsnullwhen the resolved node can't join one. It now falls back to creating a fresh gallery at the cursor.Cannot read properties of null (reading 'selectEnd')—CodeNodeInserter.insertNodesassumed a node at the caret after inserting; guarded.Tests
Three of the four regressions are covered by Playwright tests in
test/browser/tests/attachments/upload_failure_guards.test.js, each driving a real editor in a real browser:Contents— remove the editor element (real teardown), then callContents#uploadFileson the still-exposed instance, the way document-level drop zones do.Contents#uploadFiles; previously the null caret crashedCodeNodeInserter(covers Copilot's request for a regression test on this path).The gallery-splice case stays a unit test in
test/javascript/unit/editor/upload_files.test.js: the state it exercises — a selection whose first node is a previewable image while its anchor resolves to a node that can't join a gallery — comes from stale selection restoration during drag-and-drop races and can't be constructed organically; Lexical normalizes the anchor of every selection you can produce in a browser onto the attachment itself (verified by probing backwards, forward-from-root, and select-all selections). Per Copilot's note, its stub now resolves the node at call time inside the calling update instead of reusing a node captured from an earlier read.Red/green verified by reverting the three
src/files tomain: all 3 browser tests fail (the node-selection one with no insertion, the other two with the exact productionTypeErrors) and the unit test fails withCannot read properties of null (reading 'splice'); with the fix restored, 3/3 browser tests pass (chromium and firefox locally) and 1/1 unit test passes.Also fixed the
does not supports attachmentsconsole-warning grammar flagged by Copilot.