Skip to content

Silent Data Loss & Broken Error Contract in Saver.save()Β #3030

Description

@codeCraft-Ritik

2. Silent Data Loss & Broken Error Contract in Saver.save()

πŸ“ Affected Locations

  • src/components/modules/saver.ts (Lines 34–48)
  • src/components/modules/api/saver.ts (Lines 27–38)

πŸ” Deep Technical Diagnosis

In src/components/modules/saver.ts:

// src/components/modules/saver.ts: Lines 34-48
public async save(): Promise<OutputData> {
  const { BlockManager, Tools } = this.Editor;
  const blocks = BlockManager.blocks,
      chainData = [];

  try {
    blocks.forEach((block: Block) => {
      chainData.push(this.getSavedData(block));
    });

    const extractedData = await Promise.all(chainData) as Array<Pick<SavedData, 'data' | 'tool'>>;
    const sanitizedData = await sanitizeBlocks(extractedData, (name) => {
      return Tools.blockTools.get(name).sanitizeConfig;
    });

    return this.makeOutput(sanitizedData);
  } catch (e) {
    _.logLabeled(`Saving failed due to the Error %o`, 'error', e);
    // BUG: No return statement, no rethrow, no Promise.reject!
    // Silently completes and returns `undefined`!
  }
}

And in the public API exposed to developers (src/components/modules/api/saver.ts):

// src/components/modules/api/saver.ts: Lines 27-37
public save(): Promise<OutputData> {
  const errorText = 'Editor\'s content can not be saved in read-only mode';

  if (this.Editor.ReadOnly.isEnabled) {
    _.logLabeled(errorText, 'warn');
    return Promise.reject(new Error(errorText));
  }

  return this.Editor.Saver.save();
}

πŸ’₯ Real-World Impact (Catastrophic Data Loss)

When an exception occurs during block saving or sanitization (for instance, a custom tool throws in its save() method, or a circular JSON structure is encountered):

  1. Saver.save() swallows the exception and returns undefined.
  2. The editor.save() Promise resolves to undefined instead of rejecting.
  3. Typical application backend code looks like:
    try {
      const output = await editor.save();
      // If saving fails, the developer expects `catch` to trigger!
      // Instead, output is `undefined`!
      await api.saveArticle({ id: articleId, content: output });
    } catch (error) {
      toast.error("Saving failed!");
    }
  4. The database receives { content: undefined } (or null) and overwrites the existing document, wiping out all previously saved user content!
  5. Violates Promise conventions and the TypeScript contract declaring save(): Promise<OutputData>.

πŸ’‘ Proposed Solution & Patch

src/components/modules/saver.ts

@@ -44,6 +44,7 @@ export default class Saver extends Module {
       return this.makeOutput(sanitizedData);
     } catch (e) {
       _.logLabeled(`Saving failed due to the Error %o`, 'error', e);
+      throw e;
     }
   }

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions