faktorei

Engineering · first post

The footer that lied

A subtotal that appeared in the wrong place on every page — a bug that is fixed upstream, and still in the box most people install.

A multi-page invoice needs a carried forward subtotal: the running total from the previous page, printed so a reader never loses the thread. The textbook XSL-FO tool is retrieve-table-marker — you bind a marker to each line's running total and retrieve, in a footer, "the last one that ended on this page." The spec even ships a figure of markers retrieved across a two-page table's header and footer. It did not work.

On Apache FOP 2.8 the footer showed the wrong number on every page. Page 1's footer read 63.05 — on a page whose lines summed past seven thousand; it was row 1's running total, not the last one carried across the break. We mapped it row by row: with retrieve-position-within-table="last-ending" (and identically with last-starting), page 1 returned row 1's marker, page 2 the first row starting on page 2, and so on. FOP was resolving every requested position as first-starting. The API accepts your request and ignores it.

The fix is to stop fighting it: move the carried total to the header of each continuation page, where "the last marker from the previous page" is exactly "first-starting on this page" — the one value FOP will reliably hand you. The quiet virtue of this design is that it is correct under both the buggy and the conformant reading, so it needs no version detection and nothing to unwind if the bug ever goes away. A second trap lurks there: FOP fixes a repeated header's height from its static layout, before any marker is retrieved, so a header cell containing only the retrieve-marker measures zero height, and the carried total overflows onto the first line of every continuation page. The cure is a static non-breaking space beside the retrieve-marker so the cell has height before the value arrives. That one is real on every FOP version — it is about static layout measurement, not marker position.

We wrote it up to file upstream. Before filing, we re-verified on the version our own engine ships — FOP 2.10 — with a 60-row reducer, rendered through FOP's own CLI and read back page by page. The whole test is this matrix (a 60-row table breaks across three A4 pages at rows 1–27, 28–54, 55–60):

retrieve-position-within-tablepage 1page 2page 3reading
first-startingROW-1ROW-28ROW-55first row on the page ✓
last-startingROW-27ROW-54ROW-60last row starting on the page ✓
last-endingROW-27ROW-54ROW-60last row ending on the page ✓

first-starting returns ROW-1; last-ending returns ROW-27 — different values on the same page. FOP 2.10 honors the requested position. The bug is silently fixed: no changelog entry, no JIRA issue we could find, no note anywhere. So we did not file it — you cannot report a bug that the current release resolves correctly.

Here is why this is a post and not a footnote. FOP 2.10 fixed it, but Ubuntu 24.04 LTS — the current LTS — ships FOP 2.8. So does every CI image and distro-package deployment built on it, our own reference validation gate included. The bug is fixed upstream and still live in the most commonly packaged FOP in the world. If your carried-forward footer shows the wrong subtotal, you are not misreading the spec — your packaged FOP predates the fix, and the header-side design below is your way out.

Two things stay. The header-side brought-forward design stays — it is still the right choice for anyone rendering on packaged FOP, and it costs nothing on fixed versions. And the 60-row reducer stays in the tree as a regression harness, re-run on every FOP bump: the day a future version regresses, the matrix above changes and we find out before our users do. Verifying before filing is the same habit as verifying before we tag a release — and this time it kept us from publicly reporting a bug that was fixed two releases ago.