Skip to main content

A Broken EPUB Gets Fixed Before It Gets Read

Updated 9 min readMarkdown

Upload a broken EPUB and one of two things happens. Either the book refuses to open at all, or it opens with something quietly patched over: a chapter that comes out blank, a table of contents that says it could not be found, a cover that turns out to be the first large picture in the book. The difference between those two outcomes is decided before a single paragraph reaches the screen. Most of the parser's work is not reading. It is repair, done early, in a fixed order, and it stops at a fixed line.

The two kinds of broken

An EPUB is a zip file holding a package document, a manifest of parts, a reading order, and a table of contents. Every one of those layers can be wrong in a real book made by a real tool. A directory name can differ in case from what the package document says. A chapter file listed in the manifest can be missing from the zip. A table of contents can point at a chapter the manifest never lists.

The parser treats these differently. Some breaks kill the whole open. Others get absorbed, one at a time, and the book goes on. The rule behind the split is simple: anything needed to know what the book is gets no second chance; anything needed only to show one piece of it gets patched or dropped, and the rest of the book still opens.

That is why repair has to happen before reading. Once the parser starts serving chapters, a missing file or a bad link is a hole in front of the reader. Before that point, the same defect is just an entry in a list that can be skipped, guessed at, or replaced.

The zip and the package file get no second chance

The first gate is the zip itself. If the file is not a zip at all, or it is too large for the zip reader, the parser raises an error and stops. There is no partial book, no first chapter served while the rest fails. The same goes for the container document that names the package file: if it cannot be read or parsed, the open fails outright.

The one exception at this level is case. A package document can point at ops/fb.opf while the zip actually holds OPS/fb.opf. The parser tries the path as written, and on failure retries with the directory uppercased. If that also fails, the open fails. It is a single, specific retry, not a search: the parser does not walk the zip looking for a file with a similar name.

The same hard line applies to the table of contents file when one is named. If the spine names a contents file and that file cannot be read from the zip, the open fails. The EPUB3 navigation document is only consulted when no contents file is named at all. A named-but-broken contents file does not fall back to the navigation document; a missing one does.

An outer gate sits even earlier. Before parsing starts, a file over the configured parse size is refused outright, before any repair is attempted.

The XML gets rewritten before it gets parsed

Once the zip and the package file are settled, the parser starts rewriting text before it parses it. The rewrites are small, targeted, and each one exists because a real book broke on exactly that spot.

One tool writes mlns where the package tag needs xmlns, a missing letter that leaves the parser unable to find anything in the document. The repair fixes that attribute inside the package tag only, and only once. Another book's comments contain double hyphens, which XML forbids inside a comment; the repair strips comments out rather than trying to fix them. A third has bare ampersands in its text, which make the XML parser complain about an unnamed entity; the repair escapes a bare & while leaving legitimate entities like & and numeric references alone.

That last rule carries its own history. The earlier version escaped every ampersand, which turned a correct & into & and showed the reader a literal & on the page. The repair itself had to be repaired: escape only the ampersand that is not already part of an entity. A fix that touches everything it matches is not a fix.

The same tolerant attitude extends to where things sit in the package document. The manifest and the spine are searched as descendants, not just as direct children, because some broken package documents nest them inside the metadata section. A misplaced manifest is still a manifest.

A missing piece does not have to sink the book

Past the package document, the parser gets forgiving in a different way: it stops demanding that every part be present and correct.

A manifest entry whose file is missing from the zip does not abort the load. The failure is logged, the entry keeps a blank where its content would be, and the book opens. The reader who turns to that chapter gets nothing for it, but the rest of the book works. A wrong media type gets the same treatment at a smaller scale: image/jpg is corrected to image/jpeg on the spot, because people write the wrong content type and the picture is fine.

The table of contents has the most fallbacks, because it is the part readers notice first. If the contents file parses as XML, good. If it fails — a title containing angle brackets, like a book title written with < and > around it, will do that — the parser tries again as HTML, which tolerates the brackets. If the contents file has no navigation map at all, the parser does not leave the reader with nothing: it builds a single entry that says the contents could not be found and points it at the first chapter in the reading order, so the book still opens at chapter one.

For EPUB3 books, the navigation document gets its own chain of guesses. A contents entry that actually points at a navigation document is skipped when looking for the old-style contents file, so it is not parsed as something it is not. When the navigation document is used, a missing type marker does not stop it: if no navigation block is marked as the table of contents, the first block containing a list is used. A list wrapped in an extra layer is accepted even though the specification wants it as a direct child. And if navigation parsing fails entirely, the result is the same single entry pointing at the first chapter, not an error.

Two smaller repairs keep the contents and the chapters lined up. Contents links are rewritten from the contents file's directory to the package document's directory, digesting any ../ segments, so a link that was correct relative to its own file still matches the manifest. A contents entry that points nowhere in the reading order is kept, with its position marked as unknown, rather than dropped. And a chapter file that sits in the zip but was never listed in the manifest is not lost: when a contents link asks for it, the parser reads it from the zip and adds it to the manifest on the spot.

The cover goes through a similar chain. The parser looks first for the EPUB3 cover-image property, then for the EPUB2 metadata entry, then for an item literally called cover-image, then for a handful of familiar ids, and finally for the first image in the manifest large enough to plausibly be a cover — over 50 KB, which rules out small icons and decorations. Each step only runs if the previous one found nothing. A book with no cover anywhere in that chain simply has no cover, and that is treated as normal.

The chapter itself gets patched on the way to the screen

The last repairs happen chapter by chapter, at the moment the text is prepared for display, and they are about the browser as much as about the book.

EPUB chapters are XHTML, but they are served as HTML, and HTML does not treat a self-closing tag on an ordinary element the way XHTML does. A <pre/> in a chapter is a legal empty element in XHTML; in HTML it is an opening tag that never closes, and it swallows everything after it. In one book this left the whole chapter under a single <pre> element holding hundreds of paragraphs, displayed as one endless line in a monospace font. The repair rewrites self-closing non-void elements into explicit pairs before the chapter goes out — <pre/> becomes <pre></pre> — while leaving genuinely void elements like <br> and <img> alone, since rewriting those would add a second line break or image.

The same pass strips the XML declaration and the doctype from the chapter. That sounds cosmetic, and it is deliberate: without a doctype the browser falls back to quirks mode, where a body set to full height actually fills the viewport, which is what full-page picture chapters in these books expect. Keeping the doctype would put the browser in standards mode, where the same chapter needs a height set on the html element that the book never provides, and the picture shows at its natural size instead of filling the page.

Line numbers are added by pattern matching on the raw bytes, not by parsing and re-serializing the chapter. That matters for broken books: a chapter with malformed markup still gets its line numbers, because nothing in the process requires the chapter to parse cleanly first. And if that step fails for any reason, the chapter still goes out, unchanged.

Where the patching stops

So the line is drawn here. The zip, the container document, and the package file must be readable, or there is no book. Everything past that line — a missing chapter file, a contents file that will not parse, a cover that cannot be found, a chapter full of self-closing tags — is handled by guessing, substituting, or leaving a blank, and the book opens anyway.

The reader never sees most of this. They see a book that opens, a chapter that is blank, a contents page that says it could not be found, or a cover that is not quite the cover. Each of those is the visible edge of a repair that already happened, before the first paragraph was read.

About the author

  • Writing about ebook technology.

No comments yet. Be the first to comment.