Repository navigation
Diagnostics and Troubleshooting
Nothing fails silently β but you do need to know where to look. Three layers: exceptions (structural), diagnostics (data/policy), events/logs (observability).
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"]
// 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));
});| 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 |
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
My HTML shows as <b> β 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.
- Reproduce with
Engine::create(['debug' => true, 'log' => ['driver' => 'array']])and dumpdiagnostics()->all(). -
bin/htmlphp lintthe template;config:dumpthe effective config. - Search issues β open one
with the diagnostic output, template snippet, and
version()banner. - Security? Private report β see Security β Reporting.
HTML.PHP 5.0.0 Β· MIT Β© Mikhail Deynekin Β· Repository Β· Releases Β· Report a security issue
Wiki pages mirror docs/ β found a mistake? Edit the page or open an issue.
π Home Problem Domain Getting Started
Template Syntax Expressions & Math SSI Guide I18n Guide WordPress Integration CLI
Configuration API Cheatsheet Diagnostics & Troubleshooting
Architecture Security Model Testing & CI
πΊοΈ ROADMAP Migration 4.x β 5.0 Migration 3.x β 4.0 Contributing Repository β