Skip to content

mergeApiIntegrationsIntoYaml throws "Document with errors cannot be stringified" on a malformed existing integrations file #424

Description

@tkislan

Summary

mergeApiIntegrationsIntoYaml(existingContent, apiIntegrations) in @deepnote/database-integrations throws Error: Document with errors cannot be stringified when existingContent is a readable but malformed YAML file — e.g. a file containing git merge-conflict markers, or a YAML typo. Instead of regenerating the file, the call crashes.

Because this is the primitive that merges API integrations into the user's local integrations file (its own doc comment says it "mirrors what deepnote integrations pull does to the YAML file"), a deepnote integrations pull run against a locally-corrupted file would fail hard rather than heal it.

Reproduction

Against @deepnote/database-integrations@1.5.0 (node 22, yaml@2.8.3):

const { mergeApiIntegrationsIntoYaml, parseIntegrationsDocument } = require('@deepnote/database-integrations')

const conflict = [
  'integrations:',
  '<<<<<<< HEAD',
  '  - id: a',
  '=======',
  '  - id: b',
  '>>>>>>> branch',
  '',
].join('\n')

mergeApiIntegrationsIntoYaml(conflict, [])              // throws
mergeApiIntegrationsIntoYaml('a: b: c\n', [])           // throws (plain YAML typo)
mergeApiIntegrationsIntoYaml('integrations: []\n', [])  // OK
parseIntegrationsDocument(conflict)                     // returns a Document with errors.length === 6 (NOT null)

Observed output:

mergeApiIntegrationsIntoYaml(conflict-markers, []) => THREW: Error: Document with errors cannot be stringified
mergeApiIntegrationsIntoYaml("a: b: c", [])        => THREW: Error: Document with errors cannot be stringified
mergeApiIntegrationsIntoYaml("integrations: []", []) => OK
parseIntegrationsDocument(conflict)                => Document, errors.length=6

Root cause

parseIntegrationsDocument uses yaml.parseDocument(...), which collects parse errors onto doc.errors rather than throwing or returning null; it only returns null for empty/whitespace content:

function parseIntegrationsDocument(content) {
  if (!content.trim()) return null
  return parseDocument(content, { strict: true, version: '1.2' })
}

mergeApiIntegrationsIntoYaml then does:

const doc = (existingContent != null ? parseIntegrationsDocument(existingContent) : null) ?? createNewDocument()
// ... mergeApiIntegrationsIntoDocument(doc, apiIntegrations) ...
return { content: serializeIntegrationsDocument(doc), /* ... */ }

For malformed input, parseIntegrationsDocument returns a non-null errored Document, so ?? createNewDocument() never fires. serializeIntegrationsDocument is doc.toString(), and yaml's Document.toString() throws Document with errors cannot be stringified when doc.errors.length > 0.

(A related case: a well-formed file whose integrations: value is not a sequence throws InvalidIntegrationsTypeError via getOrCreateIntegrationsFromDocument.)

Impact

  • Any consumer that merges into a possibly-corrupt existing file crashes instead of self-healing. The most realistic trigger is a git merge conflict in a version-controlled integrations file (or a manual edit typo) — precisely the situation for a file that lives in a user's repo.

Suggested fix

Treat a malformed/errored existing document as "start fresh" instead of trying to serialize it. In mergeApiIntegrationsIntoYaml, fall back to createNewDocument() when the parsed document is null or has doc.errors.length > 0 (and handle the non-sequence integrations: case). Alternatively, have parseIntegrationsDocument return null on parse errors so the existing ?? createNewDocument() fallback already covers it.

Notes / caveats

  • The CLI/extension user-facing impact is inferred from the doc comment on mergeApiIntegrationsIntoYaml ("mirrors what deepnote integrations pull does to the YAML file"). I verified the library primitive throws; I did not trace the exact pull call path, so it's worth confirming whether pull already wraps this.

Filed with an automated reproduction via Claude Code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions