mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(ui): catch marked.js parse errors to prevent Control UI crash (#36445)
- Prevent Control UI session render crashes when `marked.parse()` encounters pathological recursive markdown by safely falling back to escaped `<pre>` output. - Tighten markdown fallback regression coverage and keep changelog attribution in sync for this crash-hardening path. Co-authored-by: Bin Deng <dengbin@romangic.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -35,6 +35,8 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/xAI tool-call argument decoding: decode HTML-entity encoded xAI/Grok tool-call argument values (`&`, `"`, `<`, `>`, numeric entities) before tool execution so commands with shell operators and quotes no longer fail with parse errors. (#35276) Thanks @Sid-Qin.
|
||||
- Agents/thinking-tag promotion hardening: guard `promoteThinkingTagsToBlocks` against malformed assistant content entries (`null`/`undefined`) before `block.type` reads so malformed provider payloads no longer crash session processing while preserving pass-through behavior. (#35143) thanks @Sid-Qin.
|
||||
- Gateway/Control UI version reporting: align runtime and browser client version metadata to avoid `dev` placeholders, wait for bootstrap version before first UI websocket connect, and only forward bootstrap `serverVersion` to same-origin gateway targets to prevent cross-target version leakage. (from #35230, #30928, #33928) Thanks @Sid-Qin, @joelnishanth, and @MoerAI.
|
||||
- Control UI/markdown parser crash fallback: catch `marked.parse()` failures and fall back to escaped plain-text `<pre>` rendering so malformed recursive markdown no longer crashes Control UI session rendering on load. (#36445) Thanks @BinHPdev.
|
||||
- Control UI/markdown fallback regression coverage: add explicit regression assertions for parser-error fallback behavior so malformed markdown no longer risks reintroducing hard-crash rendering paths in future markdown/parser upgrades. (#36445) Thanks @BinHPdev.
|
||||
- Web UI/config form: treat `additionalProperties: true` object schemas as editable map entries instead of unsupported fields so Accounts-style maps stay editable in form mode. (#35380, supersedes #32072) Thanks @stakeswky and @liuxiaopai-ai.
|
||||
- Feishu/streaming card delivery synthesis: unify snapshot and delta streaming merge semantics, apply overlap-aware final merge, suppress duplicate final text delivery (including text+media final packets), prefer topic-thread `message.reply` routing when a reply target exists, and tune card print cadence to avoid duplicate incremental rendering. (from #33245, #32896, #33840) Thanks @rexl2018, @kcinzgg, and @aerelune.
|
||||
- Feishu/group mention detection: carry startup-probed bot display names through monitor dispatch so `requireMention` checks compare against current bot identity instead of stale config names, fixing missed `@bot` handling in groups while preserving multi-bot false-positive guards. (#36317, #34271) Thanks @liuxiaopai-ai.
|
||||
@@ -349,6 +351,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Cron/store migration: normalize legacy cron jobs with string `schedule` and top-level `command`/`timeout` fields into canonical schedule/payload/session-target shape on load, preventing schedule-error loops on old persisted stores. (#31926) Thanks @bmendonca3.
|
||||
- Tests/Windows backup rotation: skip chmod-only backup permission assertions on Windows while retaining compose/rotation/prune coverage across platforms to avoid false CI failures from Windows non-POSIX mode semantics. (#32286) Thanks @jalehman.
|
||||
- Tests/Subagent announce: set `OPENCLAW_TEST_FAST=1` before importing `subagent-announce` format suites so module-level fast-mode constants are captured deterministically on Windows CI, preventing timeout flakes in nested completion announce coverage. (#31370) Thanks @zwffff.
|
||||
- Control UI/markdown recursion fallback: catch markdown parser failures and safely render escaped plain-text fallback instead of crashing the Control UI on pathological markdown history payloads. (#36445, fixes #36213) Thanks @BinHPdev.
|
||||
|
||||
## 2026.3.1
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { marked } from "marked";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { toSanitizedMarkdownHtml } from "./markdown.ts";
|
||||
|
||||
describe("toSanitizedMarkdownHtml", () => {
|
||||
@@ -82,4 +83,36 @@ describe("toSanitizedMarkdownHtml", () => {
|
||||
// Pipes from table delimiters must not appear as raw text
|
||||
expect(html).not.toContain("|------|");
|
||||
});
|
||||
|
||||
it("does not throw on deeply nested emphasis markers (#36213)", () => {
|
||||
// Pathological patterns that can trigger catastrophic backtracking / recursion
|
||||
const nested = "*".repeat(500) + "text" + "*".repeat(500);
|
||||
expect(() => toSanitizedMarkdownHtml(nested)).not.toThrow();
|
||||
const html = toSanitizedMarkdownHtml(nested);
|
||||
expect(html).toContain("text");
|
||||
});
|
||||
|
||||
it("does not throw on deeply nested brackets (#36213)", () => {
|
||||
const nested = "[".repeat(200) + "link" + "]".repeat(200) + "(" + "x".repeat(200) + ")";
|
||||
expect(() => toSanitizedMarkdownHtml(nested)).not.toThrow();
|
||||
const html = toSanitizedMarkdownHtml(nested);
|
||||
expect(html).toContain("link");
|
||||
});
|
||||
|
||||
it("falls back to escaped plain text if marked.parse throws (#36213)", () => {
|
||||
const parseSpy = vi.spyOn(marked, "parse").mockImplementation(() => {
|
||||
throw new Error("forced parse failure");
|
||||
});
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const input = `Fallback **probe** ${Date.now()}`;
|
||||
try {
|
||||
const html = toSanitizedMarkdownHtml(input);
|
||||
expect(html).toContain('<pre class="code-block">');
|
||||
expect(html).toContain("Fallback **probe**");
|
||||
expect(warnSpy).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
parseSpy.mockRestore();
|
||||
warnSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -110,11 +110,20 @@ export function toSanitizedMarkdownHtml(markdown: string): string {
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
const rendered = marked.parse(`${truncated.text}${suffix}`, {
|
||||
renderer: htmlEscapeRenderer,
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
}) as string;
|
||||
let rendered: string;
|
||||
try {
|
||||
rendered = marked.parse(`${truncated.text}${suffix}`, {
|
||||
renderer: htmlEscapeRenderer,
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
}) as string;
|
||||
} catch (err) {
|
||||
// Fall back to escaped plain text when marked.parse() throws (e.g.
|
||||
// infinite recursion on pathological markdown patterns — #36213).
|
||||
console.warn("[markdown] marked.parse failed, falling back to plain text:", err);
|
||||
const escaped = escapeHtml(`${truncated.text}${suffix}`);
|
||||
rendered = `<pre class="code-block">${escaped}</pre>`;
|
||||
}
|
||||
const sanitized = DOMPurify.sanitize(rendered, sanitizeOptions);
|
||||
if (input.length <= MARKDOWN_CACHE_MAX_CHARS) {
|
||||
setCachedMarkdown(input, sanitized);
|
||||
|
||||
Reference in New Issue
Block a user