Skip to content

[Bug] Backslashes in autolink URLs double on every round-trip → exponential growth #2349

@kessiler

Description

@kessiler

Initial checklist

  • I agree to follow the code of conduct
  • I searched issues and discussions and couldn't find anything (linked upstream below)

Affected packages and versions

  • @milkdown/crepe 7.20.0
  • @milkdown/transformer 7.20.0

The bug originates in mdast-util-to-markdown (transitive dep) and is already filed there as syntax-tree/mdast-util-to-markdown#73. Filing here so Milkdown users hitting this know about it, and in case Milkdown wants to apply a workaround in @milkdown/transformer while the upstream fix lands.

Link to runnable example

Reproducible directly in the official playground: https://milkdown.dev/playground

Steps to reproduce

  1. Open https://milkdown.dev/playground
  2. Clear both panes.
  3. Paste this exact markdown into the right (markdown source) pane:
    <https://example.com/page.\>
    
    That's an autolink with one trailing backslash before >.
  4. Click into the left (Crepe) editor.
  5. Press End, then Enter (any action that fires markdownUpdated).
  6. Look at the right pane. The autolink is now <https://example.com/page.\\> — backslashes doubled.
  7. Repeat steps 3–5 (paste the new content back). Counts go: 1 → 2 → 4 → 8 → 16 → 32 …

Pure-library reproducer (without the editor, to show this is in the serializer):

import { fromMarkdown } from 'mdast-util-from-markdown';
import { toMarkdown }   from 'mdast-util-to-markdown';

let md = '<https://example.com/page.\\>';   // 1 backslash
for (let i = 1; i <= 21; i++) {
  md = toMarkdown(fromMarkdown(md)).trim();
  console.log(`Save ${i}: ${md.match(/\\\\/g).length} backslashes, ${md.length} bytes`);
}
// Save 1: 2 backslashes
// Save 10: 1024
// Save 20: 1048576  (~1 MB)
// Save 21: ~2 MB

Expected behavior

Autolink URL round-trips verbatim. <https://example.com/page.\> parses, then re-serializes back to <https://example.com/page.\>.

Per CommonMark §6.7, content inside <...> autolinks is interpreted literally — backslash escapes are not processed. So the serializer should not be inserting extra backslashes for characters inside an autolink destination.

Actual behavior

Every backslash in the autolink URL is doubled on each round-trip. Because the just-added backslash is itself followed by another backslash (which is ASCII punctuation), the next round-trip doubles again. Result is exponential growth in stored content.

Real-world impact

We hit this in production: a single stray \ in an evidence document's autolink URL ballooned to ~2 MB of consecutive backslashes after roughly 21 user edits. Database row size grew without bound.

The bug is particularly nasty because:

  • It only triggers for autolinks (<URL>), not [text](url) or ![alt](url) — those round-trip cleanly through character escapes, so it's easy to miss in tests.
  • It needs \ followed by ASCII punctuation inside the autolink — once seeded, every subsequent edit doubles silently.

Runtime

Chrome (also Firefox, Safari — pure JS bug)

OS

Any (reproduced on macOS and Linux)

Root cause (already isolated)

In mdast-util-to-markdown/lib/util/safe.js's escapeBackslashes(), every existing \ followed by ASCII punctuation gets a fresh \ prepended. Since CommonMark forbids escapes inside autolinks, the parser keeps \ literal, so the count compounds.

See syntax-tree/mdast-util-to-markdown#73 for upstream tracking.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions