Skip to content

fix(epub): retry a malformed XHTML section as HTML in loadDocument - #70

Merged
chrox merged 1 commit into
mainfrom
fix/load-document-xhtml-fallback
Aug 11, 2026
Merged

fix(epub): retry a malformed XHTML section as HTML in loadDocument#70
chrox merged 1 commit into
mainfrom
fix/load-document-xhtml-fallback

Conversation

@chrox

@chrox chrox commented Aug 11, 2026

Copy link
Copy Markdown
Member

Problem

A manifest can declare a section as application/xhtml+xml while the file is not well-formed XML. Adobe InDesign and Digital Editions ship an unclosed <meta charset="utf-8"> constantly. The XML parser then hands back a parsererror document whose body is null.

The render path already recovers from this. loadReplaced reparses as text/html when it sees a parsererror or a namespace-less root:

if (mediaType === MIME.XHTML && (doc.querySelector('parsererror')
|| !doc.documentElement?.namespaceURI)) {
    item.mediaType = MIME.HTML
    doc = new DOMParser().parseFromString(str, item.mediaType)
}

loadDocument, which backs Section.createDocument(), did not, so every off-screen consumer got the error document instead of the chapter.

Impact

Readest converts KOReader CREngine XPointers through createDocument() whenever the target section is not the rendered one, and walks document.body.children to resolve the path. Against a body-less error document that threw:

TypeError: Cannot read properties of null (reading 'children')

which silently killed cross-device reading progress sync.

Fix

Apply the same fallback in loadDocument.

Verification

Confirmed in real Chromium and WebKit (via Playwright) that for such a file doc.body === null and doc.querySelector('parsererror') matches, so the same test loadReplaced uses is the right one here. Covered by a regression test in the Readest repo (foliate-epub-malformed-xhtml.test.ts) that builds an EPUB with a malformed chapter and asserts both createDocument() and the full XPointer conversion; it reproduces the TypeError verbatim without this change.

Refs: readest/readest#5625

A manifest can declare a section as application/xhtml+xml while the file
is not well-formed XML. Adobe InDesign and Digital Editions ship an
unclosed `<meta charset="utf-8">` constantly. The XML parser then hands
back a `parsererror` document whose `body` is null.

The render path already recovers from this: `loadReplaced` reparses as
text/html when it sees a `parsererror` or a namespace-less root.
`loadDocument`, which backs `Section.createDocument()`, did not, so every
off-screen consumer got the error document instead of the chapter.

Readest converts KOReader CREngine XPointers through `createDocument()`
whenever the target section is not the rendered one, and walks
`document.body.children` to resolve the path. Against a body-less error
document that threw `TypeError: Cannot read properties of null (reading
'children')`, which silently killed cross-device reading progress sync.

Refs: readest/readest#5625
@chrox
chrox merged commit 63a2eb1 into main Aug 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant