Skip to main content

Reading a render's problems

A template can render without any error at all and still produce the wrong PDF — an asset that never arrived, a script that threw, a font the image does not have. A debug render watches the browser while it works and reports what it noticed. This page is about reading that report: where it comes from, what it looks like, and what each finding means.

Where a report comes from

Three surfaces produce the same findings:

  • pdf-server debug render prints them and writes them into the bundle it produces — see CLI & configuration.
  • The /_debug bundle routes return them inside the archive — see OpenAPI / Swagger. They exist only on a server started with --debug-enabled.
  • The tester UI's Problems tab lists them for the render you are looking at — see Tester UI.

An ordinary /templates/…/render request reports nothing. Collecting diagnostics costs browser round trips, so a production render subscribes to none of it.

The two artifacts

Every bundle carries both reports, whatever --artifacts asked for — they are the one thing a debug render always produces:

FileFor
problems.txtreading — one line per finding, aligned into columns
problems.jsonlmachines — one JSON object per line, in the same order

The text report

A clean render is a single line:

ok pages=1 no problems

Anything else is a header line followed by one line per finding:

pages=1
! console-error fixture: console error
! js-exception Error: fixture: uncaught exception
~ console-warn fixture: console warning

A render that produced no document at all is headed by its failure instead of by measurements, and carries the reason as a finding of its own:

failed (data) pages=0
! render-failed data validation error: invalid data schema: missing properties: 'name'

The word ok appears only on a render that was actually judged, so a clean first line and a non-zero exit code can never disagree. The classes are listed under CLI & configuration.

The first character is the severity — ! error, ~ warning, . info — so the worst findings are greppable. The columns after it are the kind, the page (p2, or p2/3 when a finding spans pages), a kind-specific detail summary, where it happened (a CSS selector, or the URL for a network finding), and the offending text in quotes. A column is left empty when the finding has nothing to put there, so the report stays aligned and diffable.

The JSON report

One object per line, the same findings in the same order:

{"kind":"http-error","severity":"error","source":"browser","detail":{"status":404,"url":"http://…/assets/logo.png"}}
FieldAlwaysMeaning
kindyesWhich finding this is — the table below
severityyeserror, warn or info
sourceyesWho noticed: browser, rule, custom or engine
detailyesKind-specific fields; the table below names them
pageno1-based page the finding sits on, when it has one
selnoCSS selector of the element, when it is about one
textnoA short excerpt of the offending text

severity is what --fail-on compares against: with its default of error, a console-error fails the command and a console-warn does not. --fail-on warn fails on both, --fail-on never on neither, and the exit code is the whole point in a pipeline.

source says who is talking, and the difference is between a fact and an opinion. browser is something the browser told us about the document. rule is a built-in layout rule's reading of the measured page — inferred, not observed. custom is a rule the template itself ships (see Layout rules). engine is the renderer talking about its own run — not a defect in your template, but a fact about how the report itself was produced.

What each kind means

From the browser

KindSeveritydetailWhat it means
asset-failederrorurl, errorThe request never got an answer: an unreachable host, a refused connection, a blocked port. error is the browser's own net:: code.
http-errorerrorurl, statusThe request was answered with an error status. A 404 on an assets/… URL means the template names a file it does not ship.
js-exceptionerrormessage, stackA script threw and nothing caught it. stack is a list of frames, capped by --diagnostics-stack-frames.
console-errorerrormessageThe page called console.error.
console-warnwarnmessageThe page called console.warn.
csp-violationwarnmessageThe page's own Content-Security-Policy refused something it contains — most often an inline script.
font-substitutedwarnrequested, used, nodesThe page named a family, and the browser painted a different one — used — because the requested one is not in the image, or not in the weight or style the page asked for. Different advances mean different line breaks. A family's own weight or slope shipped under its own name (Open Sans SemiBold for Open Sans at 600) is the face that was asked for and is not reported; a width (Open Sans Condensed) is. See Assets and --diagnostics-accepted-font-substitutions.
font-defaultinfoused, nodesSome text was painted in a family the page never named, and used is what actually drew the glyphs. Not a defect on its own — a document can be happy with the browser's font — but it is the only visible trace of one that is: a stylesheet the browser could not parse names nothing, so the page renders in the default with nothing else to complain about. A page that names a family (even a generic one like sans-serif) for everything it paints reports nothing.
lifecycle-timeouterrorawaited, reachedThe page navigated but never reached the lifecycle event the render was waiting for. reached lists the events that did arrive, which is usually the answer: a page that reached networkIdle and was awaited on callback never called the callback.

From the layout rules

These read the layout snapshot — the measurement taken after the browser paginated the document — so every one of them is about the printed result rather than the markup. sel, page and text name the element, and by_px distances are CSS px.

KindSeveritydetailWhat it means
overflow-xerrorby_pxThe box reaches past the right edge of the content box by by_px. Whatever is out there is off the paper.
overflow-ywarnby_pxA box the browser cannot break hangs by_px past the bottom of the page it started on. Only reported for a box marked break-inside: avoid or a replaced element — content flowing across a break is ordinary pagination, not a defect.
break-splitwarnpagesAn element the document declared unbreakable — break-inside: avoid, in either spelling, or a replaced element — was split anyway, across the pages pages lists. The declaration is a request; a block taller than the page cannot be honoured, and this says it was not. The rule reads the declaration from computed style, so any class name works and a keep-together class without the CSS is not checked.
text-clippederrorby_pxThe browser reports the element's own content overflowing a box that hides it — part of it was never painted at all. by_px is how far that box itself reaches past the content box, which is 0 for a clipped box that sits inside the page. An element reported here is never also reported as overflow-x: the browser's own clip is the more specific finding for the same box.
zero-size-imageerrorurlA replaced element (img, svg, canvas) rendered with no area — most often an image that never loaded, in which case a http-error or asset-failed says why. url is the source it tried; it is empty for an element with none to read, such as a canvas.
outside-content-boxerrorby_px, edgeThe box strays past the content box's left or top edge by by_px. These are the two edges overflow does not cover, since it only measures excess beyond the right and the bottom.
empty-pagewarnpagePage page had no element laid out on it — a page a reader would open and find blank.
sparse-final-pageinfopage, filled_pctThe last page of a multi-page document is barely used: its content covers filled_pct of the content height, and that came out under 10%. A fact, not a verdict — plenty of documents legitimately run to a second page, and pages=N alone cannot tell two full pages from one page and a widow. It is the cue to open that page's image.
orphaned-final-blockinfopageThe last page exists to carry one block: everything on it is one top-level box and what sits inside that box. The widow a reader notices first, and the one a fill percentage cannot name — a page can measure a third full and still be one, because the block pushed onto it is a tall one. Info for the same reason as sparse-final-page: Chromium will not always honour break-after across a table boundary, and this is often not the author's to fix.
tiny-fontinfosize_pxAn element's own text is set below 8px (6pt), the floor for readable print. Only an element that paints text of its own is reported — a container inherits a size it never draws anything at. A document that meant to set type this small can read past an info line.

filled_pct is how much of the last page's content height the boxes assigned to it cover between them, counting overlapping boxes — a table and its rows — once. It is a coarse figure in one direction: the element table is a sample of the significant boxes rather than every painted node, so a page reads emptier than it looks, and the 10% threshold is set low to match. The practical consequence is that a final page holding a single row and a totals block can measure above the threshold and go unreported; the record catches the page that is nearly bare, not every page that is thin.

empty-page and sparse-final-page stand down, reporting nothing, whenever the element table is not the whole story: when pages_approximate says the printed page count and the measured geometry disagreed, and when elements_truncated says the cap dropped boxes. In both cases the elements that would have claimed the page may simply be missing from the measurement, and reporting a page full of content as blank is the one mistake a page-level rule must not make.

A cut table is reported in its own right, as diagnostics-capped with cap: elements — because standing down is itself invisible, and so is a template's own rule keyed on an element the cap dropped. Both report nothing, and nothing is what a healthy document reports too. A render whose measurement did not cover the whole document therefore never prints ok no problems.

From the template's own rules

A template can ship its own rules — see Layout rules. Their findings are reported beside the built-in ones with source: custom, and their kind, severity and detail are whatever the rule chose. One spelling is reserved:

KindSeveritydetailWhat it means
custom-rule-failederrorengine, rule, reason, and error or produced+capA rule did not run, or did not finish. A template may not emit it.

custom-rule-failed carries the engine (jq or js) and the rule's path relative to the template directory (diagnostics/totals-on-last-page.js), so a template with a directory of rules says which one it is talking about. It gives one of three reasons:

reasonMeansExtra detail
load-errorThe file could not be loaded at all: a syntax error, a missing exports.check, or a schema_version this build does not support.error
evaluation-errorIt ran and failed: a thrown error, a jq runtime error, a malformed emitted problem, or an interrupt after the rule timeout.error
volume-cap-exceededIt emitted more problems than one rule may contribute. The first cap are kept, the rest dropped.produced, cap

The render itself succeeded in every one of these cases, so failing it would be a lie — but a clean report from a rule that never ran would be worse. The failure is a problem in its own right at error severity, which is what keeps --fail-on error from passing a template nothing actually checked.

From the engine

KindSeveritydetailWhat it means
diagnostics-cappedinfocap, limit, dropped, kindA bound was reached and something was left out. The report is incomplete by exactly dropped — it never truncates silently. Raise the bound with the matching --diagnostics-* flag, or --layout-max-elements where cap is elements.
render-retriedinfoattempts, failed_attempts, earlier_problemsThe render succeeded, but not on the first attempt. The findings you are reading come from the attempt that worked.
multi-document-renderinfodocumentsThe findings come from several documents rendered under one call — which is what a composition does. Without this line the merged list would read as one document's findings.
policy-clampedinfosetting, asked, appliedThe template's params.json asked for a diagnostics setting the operator's mode did not allow. setting names it, asked is what the template wrote and applied is what the render used. A template can tighten its own gate freely; loosening it is the operator's call — see --diagnostics-template-policy.
render-failederrorclass, errorNo document was produced at all, so nothing that follows is a judgement on the template. class names which kind of thing went wrong — see CLI & configuration — and it is always the report's last record.

When the render fails

A render can fail outright — the page never reaches the event the render waits for, and the render times out. That is the case these findings are worth the most in, so it still produces a report:

  • The bundle is written, and the HTTP routes still answer 200 with it.
  • problems.txt and problems.jsonl carry what the collectors saw, including the lifecycle-timeout that says which event never arrived.
  • Both are headed by the failure rather than by a clean line: failed (render) pages=0, with a render-failed finding carrying the reason. A failure the collectors have nothing to add to — data the schema rejected, say — is reported by that finding alone, never as an empty list.
  • The artifacts the browser would have produced — document.pdf, the page images — are listed in manifest.json as unavailable, with the render error as their reason. manifest.json carries no pages.
  • The HTML parts are written anyway: they come from the template engine, which did its work before the browser ever started.
  • debug render exits 2. There is no document, so there is no verdict on the template — the report tells you what happened, the exit code tells a pipeline the check did not run.

What is never reported

  • A font substitution the page did not ask for. The browser's own default is measured in the page and never reported as a substitution: a page that set no font-family chose nothing, so nothing was refused. What did paint the text is reported instead, at info, as font-default.
  • /favicon.ico. Chromium asks for one on every top-level document; the render server answers 204 so this harness does not manufacture an http-error against every render ever made.
  • Anything a print header or footer references by URL. Chromium renders those templates as a document of their own and does not fetch remote subresources for them — the image is not merely broken, it is never requested. Inline it as a data: URI. See Header & footer.