diff --git a/src/Hyper/HtmlExceptionResponseRenderer.php b/src/Hyper/HtmlExceptionResponseRenderer.php
index 0b755a2..3b06944 100644
--- a/src/Hyper/HtmlExceptionResponseRenderer.php
+++ b/src/Hyper/HtmlExceptionResponseRenderer.php
@@ -201,172 +201,43 @@ private function buildHtmxFragment(StatusCode $status, \Throwable $e): string
return '
' . $this->escape($message) . '
';
}
+ /**
+ * Render the built-in fallback error page.
+ *
+ * Uses a pre-compiled PHP template via extract() + require, deliberately
+ * bypassing the Shodo TemplateEngine. The error page must render even
+ * when the template engine, cache, or configuration is broken.
+ */
private function buildHtml(
StatusCode $status,
string $title,
\Throwable $e,
?string $suggestion,
): string {
- $code = $status->value;
$rawMessage = $e->getMessage();
-
- // When the exception message is just the reason phrase (no custom
- // message was provided), use a friendlier default description.
if ($rawMessage === $status->reason()->value) {
$rawMessage = $this->defaultDescription($status);
}
- $message = $this->escape($rawMessage);
- $escapedTitle = $this->escape($title);
-
- $suggestionBlock = '';
- if ($this->verboseErrors && $suggestion !== null) {
- $escaped = $this->escape($suggestion);
- $suggestionBlock = "{$escaped}
";
- }
-
- $debugBlock = '';
- if ($this->debug) {
- $class = $this->escape(get_class($e));
- $file = $this->escape($e->getFile());
- $line = $e->getLine();
- $trace = $this->escape($e->getTraceAsString());
-
- $debugBlock = <<
- {$class}
- {$file}:{$line}
-
- Stack trace
- {$trace}
-
-
- HTML;
- }
-
- return <<
-
-
-
-
- {$code} {$escapedTitle}
- {$this->fonts()}
- {$this->css()}
-
-
-
-
{$code}
-
{$escapedTitle}
-
{$message}
- {$suggestionBlock}
- {$debugBlock}
-
-
-
-
- HTML;
- }
-
- private function fonts(): string
- {
- // phpcs:disable Generic.Files.LineLength.TooLong
- $families = 'family=Inter:wght@400;500&family=JetBrains+Mono:wght@400&family=Lora:wght@600';
- // phpcs:enable Generic.Files.LineLength.TooLong
-
- return <<
-
-
- HTML;
- }
-
- private function css(): string
- {
- // phpcs:disable Generic.Files.LineLength.TooLong
- $heading = "Lora,Georgia,'Times New Roman',serif";
- $body = "Inter,system-ui,-apple-system,'Segoe UI',sans-serif";
- $mono = "'JetBrains Mono','Fira Code','Source Code Pro',Consolas,monospace";
+ $variables = [
+ 'code' => $status->value,
+ 'title' => $this->escape($title),
+ 'message' => $this->escape($rawMessage),
+ 'suggestion' => ($this->verboseErrors && $suggestion !== null)
+ ? $this->escape($suggestion)
+ : null,
+ 'exception' => $this->debug ? $this->escape(get_class($e)) : null,
+ 'file' => $this->debug ? $this->escape($e->getFile()) : null,
+ 'line' => $this->debug ? $e->getLine() : null,
+ 'trace' => $this->debug ? $this->escape($e->getTraceAsString()) : null,
+ ];
- return <<
- body {
- margin:0; padding:0; min-height:100vh;
- display:flex; align-items:center; justify-content:center;
- background:#faf8f1;
- font-family:{$body}; color:#2c2a25;
- }
- .container { max-width:480px; width:100%; padding:24px; }
- .status-code {
- margin:0 0 8px; font-family:{$heading};
- font-size:48px; font-weight:600;
- line-height:1.10; letter-spacing:-0.5px; color:#b5623f;
- }
- h1 {
- margin:0 0 8px; font-family:{$heading};
- font-size:28px; font-weight:600; line-height:1.20; color:#2c2a25;
- }
- .message { margin:0; font-size:16px; line-height:1.65; color:#6b675e; }
- .suggestion {
- margin:16px 0 0; padding:14px 18px;
- border-left:3px solid #4a6fa5; background:rgba(74,111,165,0.08);
- border-radius:6px; color:#4a6fa5; font-size:14px; line-height:1.55;
- }
- .debug {
- margin-top:32px; padding-top:24px; border-top:1px solid #ddd9ce;
- }
- .debug-class {
- margin:0 0 8px; font-family:{$mono};
- font-size:14px; color:#3d3a34;
- }
- .debug-file {
- margin:0 0 16px; font-family:{$mono};
- font-size:13px; color:#6b675e;
- }
- details summary {
- cursor:pointer; font-family:{$body};
- font-size:14px; font-weight:500; color:#6b675e; margin-bottom:8px;
- }
- details pre {
- margin:0; padding:16px 20px; background:#eae6da;
- border:1px solid #ddd9ce; border-radius:6px; overflow-x:auto;
- font-family:{$mono}; font-size:13px; line-height:1.55; color:#3d3a34;
- }
- .actions { margin-top:32px; display:flex; gap:12px; }
- .actions a {
- display:inline-block; padding:10px 20px; border-radius:6px;
- border:1px solid #c4bfb3; background:transparent; color:#b5623f;
- font-family:{$body}; font-size:15px; font-weight:500;
- text-decoration:none;
- }
- .actions a:hover { background:#ece9e0; }
- @media (prefers-color-scheme: dark) {
- body { background:#1a1915; color:#e8e4db; }
- .status-code { color:#c8795a; }
- h1 { color:#e8e4db; }
- .message { color:#9c9789; }
- .suggestion {
- border-color:#6a8fc0; color:#6a8fc0;
- background:rgba(106,143,192,0.1);
- }
- .debug { border-color:#3d3a34; }
- .debug-class { color:#c4bfb3; }
- .debug-file { color:#9c9789; }
- details summary { color:#9c9789; }
- details pre {
- background:#2d2b24; border-color:#3d3a34; color:#c4bfb3;
- }
- .actions a {
- border-color:#3d3a34; color:#c8795a;
- }
- .actions a:hover { background:#2d2b24; }
- }
-
- CSS;
- // phpcs:enable Generic.Files.LineLength.TooLong
+ return (static function (string $__template, array $__variables): string {
+ extract($__variables);
+ ob_start();
+ require $__template;
+ return (string) ob_get_clean();
+ })(__DIR__ . '/Templates/compiled-error.php', $variables);
}
/**
diff --git a/src/Hyper/Templates/compiled-error.php b/src/Hyper/Templates/compiled-error.php
new file mode 100644
index 0000000..0bbcf3d
--- /dev/null
+++ b/src/Hyper/Templates/compiled-error.php
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+ = $code ?> = $title ?>
+
+
+
+
+
+
+
+
+
= $code ?>
+
= $title ?>
+
= $message ?>
+
+
= $suggestion ?>
+
+
+
+
= $exception ?>
+
= $file ?>:= $line ?>
+
+ Stack trace
+ = $trace ?>
+
+
+
+
+
+
+