Skip to content

Diagnostics and Troubleshooting

Mikhail Deynekin edited this page Oct 11, 2026 · 2 revisions

Diagnostics & Troubleshooting

Nothing fails silently β€” but you do need to know where to look. Three layers: exceptions (structural), diagnostics (data/policy), events/logs (observability).

Where errors surface

flowchart LR
    A[problem] --> B{structural?}
    B -- "unbalanced tag,\nmissing template,\nbad expression syntax" --> X["throws\nTemplateSyntaxException /\nTemplateNotFoundException"]
    B -- "missing var, denied exec,\nfailed include, unknown filter" --> D["Diagnostics\n(capped, coded)"]
    D --> L["Logger\n(log.forward_diagnostics)"]
    D --> E["event htmlphp.error\n(WP: do_action('htmlphp_error'))"]
    D --> A2["admin screen / wp htmlphp health / CLI"]
Loading
// Inspect after a render:
foreach ($engine->diagnostics()->all() as $entry) {
    // ['code' => 'missing-var', 'message' => …, 'context' => […], 'where' => 'file:line', 'time' => …]
}
$engine->diagnostics()->count('ssi-include-failed');   // by code
$engine->diagnostics()->clean();                        // bool β€” handy in tests

// React live:
$engine->events()->on('htmlphp.error', function (string $code, string $msg, array $ctx) {
    error_log("[htmlphp] {$code}: {$msg} " . json_encode($ctx));
});

Diagnostics code table

Code Typical cause First move
missing-var %{name} absent from every scope frame check the data contract; policy in syntax.missing_var
ssi-include-failed #include unresolved / outside root / denied extension / too big verify path, root, deny-list; stub= for graceful fallback
ssi-include-cycle A includes B includes A break the cycle; the guard already stopped it
ssi-limit-reached depth (>16) or tag budget (>256) exhausted raise limits.max_ssi_* deliberately, or restructure
ssi-exec-denied #exec under deny/failed allow-list/delegate said no intended β€” see SSI β†’ exec
ssi-exec-failed spawn error, timeout, non-zero exit, truncated output check binary/limits; prefer custom directives
ssi-expr-error unparseable #if expr, over-long, -A/-U used fix expression; it evaluated false
ssi-unknown-directive <!--#frobnicate--> left verbatim typo? register a custom directive
filter-unknown %{x|uppr} typo; lint catches it pre-deploy
expr-invalid (v5) malformed inline %{= …} (also over-long / too deep) the tag stayed visible in the output β€” fix the expression; lint finds it pre-deploy
expr-disabled (v5) %{= …} met syntax.expressions: false intended when the gate is closed; nothing was evaluated
regex-failed PCRE error on hostile input patterns are hardened; inspect the logged pattern
catalog-parse unreadable/malformed PO/MO/JSON catalogue validate the file; loader returned null
cache-io cache dir unwritable / driver unavailable permissions; fallback was applied
loop-guard row limit / nesting / huge range data size vs limits.max_loop_rows
template-lint lenient-mode expression problems at render run lint for the full picture
wp-bridge shortcode/part render failure, missing part check template locator roots

Lint issue codes

unknown-tag Β· unclosed-tag Β· unbalanced-tag Β· orphan-mid Β· unknown-filter Β· filter-disabled Β· filter-arg-not-numeric (warning, v5) Β· bad-expression Β· expressions-disabled (v5) Β· unresolved-include Β· unresolved-extends Β· missing-reference Β· loop-range-too-large Β· loop-signal-outside-loop Β· trans-count-without-plural Β· trans-plural-without-count Β· trans-nested-tag Β· untranslated-message Β· unknown-ssi-directive Β· ssi-missing-expr Β· ssi-include-target Β· ssi-echo-var Β· ssi-set-var Β· ssi-bad-expr Β· ssi-exec-denied (info) Β· template-unreadable

FAQ

My HTML shows as &lt;b&gt; β€” double escaping?

Auto-escape is ON by default in v4 (v3: off). You're passing pre-escaped or intentionally raw HTML data. Fix at the source: pass raw strings and let the engine escape, or mark the trusted spot %{html|raw}. Blanket auto_escape: false works but re-opens the v3 XSS model β€” last resort.

TemplateNotFoundException but the file exists

The locator only searches templates.paths (+ @namespace roots, + the including document's dir for partials) and refuses absolute paths and .. in template data. Check: path in paths? extension matches templates.extension or is written out? file inside a root (not symlinked out)? For host-provided absolute paths use renderFile() directly.

SSI includes render nothing

Walk the deny chain: (1) ssi.enabled? (2) resolved inside document_root/RequestContext? (3) extension not denied (.php, .inc, dotfiles…)? (4) under max_include_bytes? (5) cycle/depth/budget? Every refusal recorded a diagnostic β€” read $engine->diagnostics()->byCode('ssi-include-failed') first, guess second.

#exec does nothing

By design: ssi.exec_mode defaults to deny. See SSI β†’ exec policies. If you enabled allow-list: binaries must realpath/basename-match exec_cmd_allow and be executable; cgi= must be root-contained with an allowed extension.

Cache never hits

Keys include locale, escape policy, anchor file/root and a hash of the variables β€” a timestamp or nonce in $vars means every render is unique (by design). Move volatile values out of cached renders, or render cached shell + dynamic islands separately. Also check cache.enabled / RenderOptions(cache: true) and diagnostics()->byCode('cache-io').

Translations show the key/source text

In order: catalogue actually loaded? ($engine->translator()->stats(), hasCatalogue('de')) β†’ naming convention matches (messages-de.po, de.po, de/LC_MESSAGES/messages.po, .json, .php)? β†’ locale normalized the way you think (de_DE β‰  de; check fallback)? β†’ WP delegation returning the source string (that's a miss, falls through)? β†’ fuzzy entries are ignored on purpose. wp htmlphp i18n audit answers all of this in one command.

French plurals were wrong before v4

Known upstream bug: v3 shipped no French rule, defaulting to n != 1 (so 0 élément→plural form). v4 added fr/pt/kab = n > 1. If you hard-corrected around it, remove the workaround.

Blank page / white screen under WordPress

The bridge never fatals on template errors (shortcodes return '' + diagnostic). A WSOD usually means the library didn't load: check the admin notice ("library not found"), confirm lib/php83|php85/html.php or Composer autoload is present, and that PHP β‰₯ 8.3 (the 8.5 bundle parse-errors on 8.3 by design β€” use the 8.3 bundle there). wp htmlphp info when CLI works.

Long-running worker serves stale dates/templates

Flush per job/deploy: $engine->clearCache() (parse memo + locator + output cache) and Template\Expression::reset(); inject a ClockInterface so time is never accidental. Catalogues: Translator::reset() after reloading PO/JSON files.

Performance: what's slow?

$engine->stats() after a render: durationMs, per-stage counters, templates[] touched, cache hit. Then php tools/bench.php for reference numbers on your box (~1.4 k full renders/s incl. SSI+i18n on 50-row templates is the baseline shape). Big documents: enable the output cache, keep loops under the row cap, avoid per-row #exec.

Getting help

  1. Reproduce with Engine::create(['debug' => true, 'log' => ['driver' => 'array']]) and dump diagnostics()->all().
  2. bin/htmlphp lint the template; config:dump the effective config.
  3. Search issues β†’ open one with the diagnostic output, template snippet, and version() banner.
  4. Security? Private report β€” see Security β†’ Reporting.

Clone this wiki locally