Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/render-helper/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @ecency/render-helper

## 2.4.30

### Patch Changes

- Dom parser improvement (#753)

## 2.4.29

### Patch Changes
Expand Down
7 changes: 5 additions & 2 deletions packages/render-helper/dist/browser/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/render-helper/dist/browser/index.js.map

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions packages/render-helper/dist/node/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,11 @@ function createDoc(html) {
return null;
}
const cleanedHtml = removeDuplicateAttributes(html);
const doc = DOMParser.parseFromString(`<body>${cleanedHtml}</body>`, "text/html");
return doc;
try {
return DOMParser.parseFromString(`<body>${cleanedHtml}</body>`, "text/html");
} catch {
return null;
}
}
function makeEntryCacheKey(entry) {
return `${entry.author}-${entry.permlink}-${entry.last_update}-${entry.updated}`;
Expand Down
2 changes: 1 addition & 1 deletion packages/render-helper/dist/node/index.cjs.map

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions packages/render-helper/dist/node/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,11 @@ function createDoc(html) {
return null;
}
const cleanedHtml = removeDuplicateAttributes(html);
const doc = DOMParser.parseFromString(`<body>${cleanedHtml}</body>`, "text/html");
return doc;
try {
return DOMParser.parseFromString(`<body>${cleanedHtml}</body>`, "text/html");
} catch {
return null;
}
}
function makeEntryCacheKey(entry) {
return `${entry.author}-${entry.permlink}-${entry.last_update}-${entry.updated}`;
Expand Down
2 changes: 1 addition & 1 deletion packages/render-helper/dist/node/index.mjs.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/render-helper/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@ecency/render-helper",
"private": false,
"version": "2.4.29",
"version": "2.4.30",
"description": "Markdown+Html Render helper",
"repository": {
"type": "git",
Expand Down
38 changes: 38 additions & 0 deletions packages/render-helper/src/helper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,44 @@ describe('Helper Functions', () => {
expect(imgs?.length).toBeGreaterThan(0)
expect(brs?.length).toBeGreaterThan(0)
})

// Regression: @xmldom/xmldom 0.9+ throws a fatal ParseError on severely
// malformed HTML (e.g., a <p> that fails to close before </body>). The
// onError handler can't suppress the throw — only a try/catch in
// createDoc can. Before the fix, this crashed SSR for /entry/[...] pages.
// See Sentry ECENCY-NEXT-1C86.
//
// The contract these tests lock in is: createDoc must return `Document | null`
// for any input, never throw. Whether a particular malformed snippet parses
// successfully (Document) or fails (null) depends on xmldom's recovery
// heuristics, which may improve across versions — we deliberately allow
// either outcome as long as the return shape matches the type.
const expectDocumentOrNull = (result: ReturnType<typeof createDoc>) => {
if (result === null) return
expect(typeof result.getElementsByTagName).toBe('function')
}

it('should not throw on mismatched body/p tags', () => {
// The content ends with an opening <p> that has no closing tag.
// Parsing <body>...<p></body> triggers xmldom's fatalError path.
const input = '<p>oops<p>hello'
expect(() => createDoc(input)).not.toThrow()
expectDocumentOrNull(createDoc(input))
})

it('should not throw on deeply malformed HTML', () => {
// Nested unclosed tags — another pattern xmldom reports as fatal.
const input = '<div><p><span><a><b><i>no closing tags here'
expect(() => createDoc(input)).not.toThrow()
expectDocumentOrNull(createDoc(input))
})

it('should not throw on invalid XML characters', () => {
// Unescaped < followed by non-tag content is a classic fatal-error case.
const input = '<div>less than < something</div>'
expect(() => createDoc(input)).not.toThrow()
expectDocumentOrNull(createDoc(input))
})
})

describe('makeEntryCacheKey', () => {
Expand Down
17 changes: 14 additions & 3 deletions packages/render-helper/src/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,20 @@ export function createDoc(html: string): Document | null {
// This is needed because markdownToHTML can generate multiple top-level elements
// (e.g., <center>...</center><hr />) which DOMParser doesn't accept without a wrapper
// Using <body> instead of <div> prevents conflicts with <div> elements in the content
const doc = DOMParser.parseFromString(`<body>${cleanedHtml}</body>`, 'text/html')

return doc
//
// @xmldom/xmldom 0.9+ always throws a ParseError from its internal fatalError() path
// (see dom-parser.js:490) for severely malformed HTML — e.g., mismatched tags like
// <body>...<p>...</body> — regardless of the onError handler returning undefined.
// The onError handler only gets to observe; the throw still happens.
//
// Wrap in try/catch so that pathologically-malformed post bodies degrade gracefully
// (no image preload hint) instead of crashing the SSR render of /entry/[...]. Both
// callers in catch-post-image.ts already handle a null return.
try {
return DOMParser.parseFromString(`<body>${cleanedHtml}</body>`, 'text/html')
} catch {
return null
}
}

export function makeEntryCacheKey(entry: any): string {
Expand Down