From 891889021766a5933ce0208e101a7bb9434dfeae Mon Sep 17 00:00:00 2001 From: Dmitrii Bukhonov Date: Fri, 6 Mar 2026 18:42:24 +0400 Subject: [PATCH 01/17] Render exception class PHPDoc description in HTML debug output --- src/Renderer/HtmlRenderer.php | 67 +++++++++++++++++++ templates/development.php | 5 ++ tests/Renderer/HtmlRendererTest.php | 41 ++++++++++++ tests/Support/TestDocBlockException.php | 16 +++++ .../Support/TestExceptionWithoutDocBlock.php | 9 +++ 5 files changed, 138 insertions(+) create mode 100644 tests/Support/TestDocBlockException.php create mode 100644 tests/Support/TestExceptionWithoutDocBlock.php diff --git a/src/Renderer/HtmlRenderer.php b/src/Renderer/HtmlRenderer.php index a3c8eb6..a9c90e0 100644 --- a/src/Renderer/HtmlRenderer.php +++ b/src/Renderer/HtmlRenderer.php @@ -16,6 +16,7 @@ use Yiisoft\ErrorHandler\ThrowableRendererInterface; use Yiisoft\FriendlyException\FriendlyExceptionInterface; use Yiisoft\Http\Header; +use ReflectionClass; use ReflectionException; use ReflectionFunction; use ReflectionMethod; @@ -45,11 +46,17 @@ use function ob_implicit_flush; use function ob_start; use function realpath; +use function preg_match; +use function preg_replace; +use function preg_replace_callback; +use function preg_split; use function str_replace; +use function str_starts_with; use function stripos; use function strlen; use function count; use function function_exists; +use function trim; use const DIRECTORY_SEPARATOR; use const ENT_QUOTES; @@ -204,10 +211,16 @@ public function render(Throwable $t, ?ServerRequestInterface $request = null): E public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData { + $displayThrowable = $t instanceof CompositeException ? $t->getFirstException() : $t; + $exceptionDescription = $displayThrowable instanceof FriendlyExceptionInterface + ? null + : $this->getThrowableDescription($displayThrowable); + return new ErrorData( $this->renderTemplate($this->verboseTemplate, [ 'request' => $request, 'throwable' => $t, + 'exceptionDescription' => $exceptionDescription, ]), [Header::CONTENT_TYPE => self::CONTENT_TYPE], ); @@ -541,6 +554,60 @@ public function removeAnonymous(string $value): string return $anonymousPosition !== false ? substr($value, 0, $anonymousPosition) : $value; } + /** + * Extracts a user-facing description from throwable class PHPDoc. + * + * Takes only descriptive text before block tags and normalizes inline + * {@see ...}/{@link ...} annotations into markdown-friendly form. + */ + private function getThrowableDescription(Throwable $throwable): ?string + { + $docComment = (new ReflectionClass($throwable))->getDocComment(); + if ($docComment === false) { + return null; + } + + $descriptionLines = []; + foreach (preg_split('/\R/', $docComment) ?: [] as $line) { + $line = trim($line); + $line = preg_replace('/^\/\*\*?/', '', $line) ?? $line; + $line = preg_replace('/\*\/$/', '', $line) ?? $line; + $line = preg_replace('/^\*/', '', $line) ?? $line; + $line = trim($line); + + if ($line !== '' && str_starts_with($line, '@')) { + break; + } + + $descriptionLines[] = $line; + } + + $description = trim(implode("\n", $descriptionLines)); + if ($description === '') { + return null; + } + + return preg_replace_callback( + '/\{@(see|link)\s+([^\s}]+)(?:\s+([^}]+))?\}/i', + static function (array $matches): string { + $target = $matches[2]; + $label = trim($matches[3] ?? ''); + + if (preg_match('/^https?:\/\//i', $target) === 1) { + $text = $label !== '' ? $label : $target; + return '[' . $text . '](' . $target . ')'; + } + + if ($label !== '') { + return $label . ' (`' . $target . '`)'; + } + + return '`' . $target . '`'; + }, + $description, + ) ?? $description; + } + /** * Renders a template. * diff --git a/templates/development.php b/templates/development.php index 47db4e5..88e57bb 100644 --- a/templates/development.php +++ b/templates/development.php @@ -9,6 +9,7 @@ /** * @var ServerRequestInterface|null $request * @var Throwable $throwable + * @var string|null $exceptionDescription */ $theme = $_COOKIE['yii-exception-theme'] ?? ''; @@ -93,6 +94,10 @@ htmlEncode($exceptionMessage)) ?> + +
parseMarkdown($exceptionDescription) ?>
+ +
parseMarkdown($solution) ?>
diff --git a/tests/Renderer/HtmlRendererTest.php b/tests/Renderer/HtmlRendererTest.php index 3d464f0..ce4bf8d 100644 --- a/tests/Renderer/HtmlRendererTest.php +++ b/tests/Renderer/HtmlRendererTest.php @@ -15,6 +15,9 @@ use RuntimeException; use Yiisoft\ErrorHandler\Exception\ErrorException; use Yiisoft\ErrorHandler\Renderer\HtmlRenderer; +use Yiisoft\ErrorHandler\Tests\Support\TestDocBlockException; +use Yiisoft\ErrorHandler\Tests\Support\TestExceptionWithoutDocBlock; +use Yiisoft\ErrorHandler\Tests\Support\TestFriendlyException; use Yiisoft\ErrorHandler\Tests\Support\TestHelper; use function dirname; @@ -66,6 +69,44 @@ public function testVerboseOutput(): void $this->assertStringContainsString($exceptionMessage, (string) $errorData); } + public function testVerboseOutputRendersThrowableDescriptionFromDocComment(): void + { + $renderer = new HtmlRenderer(); + $exception = new TestDocBlockException('exception-test-message'); + + $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); + $result = (string) $errorData; + + $this->assertStringContainsString('
', $result); + $this->assertStringContainsString('Test summary with RuntimeException.', $result); + $this->assertStringContainsString( + 'Yii Framework', + $result, + ); + } + + public function testVerboseOutputDoesNotRenderThrowableDescriptionWhenNoDocComment(): void + { + $renderer = new HtmlRenderer(); + $exception = new TestExceptionWithoutDocBlock('exception-test-message'); + + $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); + + $this->assertStringNotContainsString('
', (string) $errorData); + } + + public function testVerboseOutputKeepsFriendlyExceptionBehaviorWithoutDescriptionDuplication(): void + { + $renderer = new HtmlRenderer(); + $exception = new TestFriendlyException('exception-test-message'); + + $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); + $result = (string) $errorData; + + $this->assertStringContainsString('
', $result); + $this->assertStringNotContainsString('
', $result); + } + public function testNonVerboseOutputWithCustomTemplate(): void { $templateFileContents = 'getMessage();?>'; diff --git a/tests/Support/TestDocBlockException.php b/tests/Support/TestDocBlockException.php new file mode 100644 index 0000000..ae13ef7 --- /dev/null +++ b/tests/Support/TestDocBlockException.php @@ -0,0 +1,16 @@ + Date: Tue, 10 Mar 2026 19:21:42 +0400 Subject: [PATCH 02/17] Escape unsafe links in exception class PHPDoc descriptions rendered in HTML debug output --- src/Renderer/HtmlRenderer.php | 52 ++++++++++++++++++- templates/development.php | 2 +- tests/Renderer/HtmlRendererTest.php | 17 ++++++ tests/Support/TestUnsafeDocBlockException.php | 14 +++++ 4 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 tests/Support/TestUnsafeDocBlockException.php diff --git a/src/Renderer/HtmlRenderer.php b/src/Renderer/HtmlRenderer.php index a9c90e0..f066a57 100644 --- a/src/Renderer/HtmlRenderer.php +++ b/src/Renderer/HtmlRenderer.php @@ -587,8 +587,8 @@ private function getThrowableDescription(Throwable $throwable): ?string return null; } - return preg_replace_callback( - '/\{@(see|link)\s+([^\s}]+)(?:\s+([^}]+))?\}/i', + $description = preg_replace_callback( + '/\{@(see|link)\s+([^\s}]+)(?:\s+([^}]+))?}/i', static function (array $matches): string { $target = $matches[2]; $label = trim($matches[3] ?? ''); @@ -606,6 +606,54 @@ static function (array $matches): string { }, $description, ) ?? $description; + + $paragraphs = preg_split('/\R\s*\R/', $description) ?: []; + $result = []; + + foreach ($paragraphs as $paragraph) { + $paragraph = trim($paragraph); + if ($paragraph === '') { + continue; + } + + $parts = preg_split( + '/(\[[^]]+]\([^)]+\)|`[^`]+`)/', + $paragraph, + -1, + PREG_SPLIT_DELIM_CAPTURE, + ) ?: []; + + $html = ''; + foreach ($parts as $part) { + if ($part === '') { + continue; + } + + if (preg_match('/^\[([^]]+)]\(([^)]+)\)$/', $part, $matches) === 1) { + if (preg_match('/^https?:\/\//i', $matches[2]) === 1) { + $html .= '' + . $this->htmlEncode($matches[1]) + . ''; + } else { + $html .= $this->htmlEncode($matches[1]) + . ' (' . $this->htmlEncode($matches[2]) . ')'; + } + + continue; + } + + if (preg_match('/^`([^`]+)`$/', $part, $matches) === 1) { + $html .= '' . $this->htmlEncode($matches[1]) . ''; + continue; + } + + $html .= $this->htmlEncode($part); + } + + $result[] = '

' . $html . '

'; + } + + return $result === [] ? null : implode("\n", $result); } /** diff --git a/templates/development.php b/templates/development.php index 88e57bb..9ba2087 100644 --- a/templates/development.php +++ b/templates/development.php @@ -95,7 +95,7 @@
-
parseMarkdown($exceptionDescription) ?>
+
diff --git a/tests/Renderer/HtmlRendererTest.php b/tests/Renderer/HtmlRendererTest.php index ce4bf8d..1e37e5e 100644 --- a/tests/Renderer/HtmlRendererTest.php +++ b/tests/Renderer/HtmlRendererTest.php @@ -19,6 +19,7 @@ use Yiisoft\ErrorHandler\Tests\Support\TestExceptionWithoutDocBlock; use Yiisoft\ErrorHandler\Tests\Support\TestFriendlyException; use Yiisoft\ErrorHandler\Tests\Support\TestHelper; +use Yiisoft\ErrorHandler\Tests\Support\TestUnsafeDocBlockException; use function dirname; use function file_exists; @@ -107,6 +108,22 @@ public function testVerboseOutputKeepsFriendlyExceptionBehaviorWithoutDescriptio $this->assertStringNotContainsString('
', $result); } + public function testVerboseOutputEscapesUnsafeThrowableDescriptionLinks(): void + { + $renderer = new HtmlRenderer(); + $exception = new TestUnsafeDocBlockException('exception-test-message'); + + $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); + $result = (string) $errorData; + + $this->assertStringNotContainsString('href="javascript:alert(1)"', $result); + $this->assertStringContainsString('Click me (javascript:alert(1))', $result); + $this->assertStringContainsString( + 'Safe link', + $result, + ); + } + public function testNonVerboseOutputWithCustomTemplate(): void { $templateFileContents = 'getMessage();?>'; diff --git a/tests/Support/TestUnsafeDocBlockException.php b/tests/Support/TestUnsafeDocBlockException.php new file mode 100644 index 0000000..625f884 --- /dev/null +++ b/tests/Support/TestUnsafeDocBlockException.php @@ -0,0 +1,14 @@ + should not survive. + * + * {@link javascript:alert(1) Click me} and {@link https://www.yiiframework.com Safe link}. + */ +final class TestUnsafeDocBlockException extends RuntimeException {} From 86d62269aacad9e945dab09550bddda0e8b930d7 Mon Sep 17 00:00:00 2001 From: dbuhonov <47827492+dbuhonov@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:31:14 +0000 Subject: [PATCH 03/17] Apply PHP CS Fixer and Rector changes (CI) --- src/Renderer/HtmlRenderer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Renderer/HtmlRenderer.php b/src/Renderer/HtmlRenderer.php index f066a57..e891e79 100644 --- a/src/Renderer/HtmlRenderer.php +++ b/src/Renderer/HtmlRenderer.php @@ -61,6 +61,7 @@ use const DIRECTORY_SEPARATOR; use const ENT_QUOTES; use const EXTR_OVERWRITE; +use const PREG_SPLIT_DELIM_CAPTURE; /** * Formats throwable into HTML string. From 70bd017fa4c31bc1edcdaa12c09e708c365bc506 Mon Sep 17 00:00:00 2001 From: Dmitrii Bukhonov Date: Tue, 10 Mar 2026 19:47:54 +0400 Subject: [PATCH 04/17] Add tests for rendering exception PHPDoc descriptions and unsafe content handling in HTML debug output --- tests/Renderer/HtmlRendererTest.php | 24 +++++++++++++++++++ .../TestEmptyDescriptionDocBlockException.php | 13 ++++++++++ .../TestInlineCodeDocBlockException.php | 12 ++++++++++ 3 files changed, 49 insertions(+) create mode 100644 tests/Support/TestEmptyDescriptionDocBlockException.php create mode 100644 tests/Support/TestInlineCodeDocBlockException.php diff --git a/tests/Renderer/HtmlRendererTest.php b/tests/Renderer/HtmlRendererTest.php index 1e37e5e..475080b 100644 --- a/tests/Renderer/HtmlRendererTest.php +++ b/tests/Renderer/HtmlRendererTest.php @@ -16,9 +16,11 @@ use Yiisoft\ErrorHandler\Exception\ErrorException; use Yiisoft\ErrorHandler\Renderer\HtmlRenderer; use Yiisoft\ErrorHandler\Tests\Support\TestDocBlockException; +use Yiisoft\ErrorHandler\Tests\Support\TestEmptyDescriptionDocBlockException; use Yiisoft\ErrorHandler\Tests\Support\TestExceptionWithoutDocBlock; use Yiisoft\ErrorHandler\Tests\Support\TestFriendlyException; use Yiisoft\ErrorHandler\Tests\Support\TestHelper; +use Yiisoft\ErrorHandler\Tests\Support\TestInlineCodeDocBlockException; use Yiisoft\ErrorHandler\Tests\Support\TestUnsafeDocBlockException; use function dirname; @@ -96,6 +98,16 @@ public function testVerboseOutputDoesNotRenderThrowableDescriptionWhenNoDocComme $this->assertStringNotContainsString('
', (string) $errorData); } + public function testVerboseOutputDoesNotRenderThrowableDescriptionWhenDocCommentHasNoDescription(): void + { + $renderer = new HtmlRenderer(); + $exception = new TestEmptyDescriptionDocBlockException('exception-test-message'); + + $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); + + $this->assertStringNotContainsString('
', (string) $errorData); + } + public function testVerboseOutputKeepsFriendlyExceptionBehaviorWithoutDescriptionDuplication(): void { $renderer = new HtmlRenderer(); @@ -124,6 +136,18 @@ public function testVerboseOutputEscapesUnsafeThrowableDescriptionLinks(): void ); } + public function testVerboseOutputRendersInlineCodeAndSeeTagWithoutLabel(): void + { + $renderer = new HtmlRenderer(); + $exception = new TestInlineCodeDocBlockException('exception-test-message'); + + $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); + $result = (string) $errorData; + + $this->assertStringContainsString('inline-code', $result); + $this->assertStringContainsString('RuntimeException', $result); + } + public function testNonVerboseOutputWithCustomTemplate(): void { $templateFileContents = 'getMessage();?>'; diff --git a/tests/Support/TestEmptyDescriptionDocBlockException.php b/tests/Support/TestEmptyDescriptionDocBlockException.php new file mode 100644 index 0000000..433f471 --- /dev/null +++ b/tests/Support/TestEmptyDescriptionDocBlockException.php @@ -0,0 +1,13 @@ + Date: Wed, 11 Mar 2026 18:28:11 +0400 Subject: [PATCH 05/17] Update throwable description handling to produce HTML fragments for error templates --- src/Renderer/HtmlRenderer.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Renderer/HtmlRenderer.php b/src/Renderer/HtmlRenderer.php index e891e79..e694efd 100644 --- a/src/Renderer/HtmlRenderer.php +++ b/src/Renderer/HtmlRenderer.php @@ -558,8 +558,15 @@ public function removeAnonymous(string $value): string /** * Extracts a user-facing description from throwable class PHPDoc. * - * Takes only descriptive text before block tags and normalizes inline - * {@see ...}/{@link ...} annotations into markdown-friendly form. + * Takes only descriptive text before block tags and converts it into an + * HTML fragment suitable for direct inclusion in the error template. + * Inline {@see ...}/{@link ...} annotations are rendered as HTML links. + * + * The returned value is an HTML snippet (for example, containing

, , + * elements) and is intended to be inserted into the template as-is, + * without additional HTML-escaping. + * + * @return string|null HTML fragment describing the throwable, or null if no description is available. */ private function getThrowableDescription(Throwable $throwable): ?string { From 1e978c3a9ca26d0010574c95dec700002a81b333 Mon Sep 17 00:00:00 2001 From: Dmitrii Bukhonov Date: Wed, 11 Mar 2026 18:42:18 +0400 Subject: [PATCH 06/17] Add tests for escaping unsafe content in exception PHPDoc descriptions rendered in HTML debug output --- tests/Renderer/HtmlRendererTest.php | 98 +++++++++++++++++++ ...estOwaspFilterEvasionDocBlockException.php | 24 +++++ .../TestUnsafeMarkdownDocBlockException.php | 26 +++++ 3 files changed, 148 insertions(+) create mode 100644 tests/Support/TestOwaspFilterEvasionDocBlockException.php create mode 100644 tests/Support/TestUnsafeMarkdownDocBlockException.php diff --git a/tests/Renderer/HtmlRendererTest.php b/tests/Renderer/HtmlRendererTest.php index 475080b..4f48f58 100644 --- a/tests/Renderer/HtmlRendererTest.php +++ b/tests/Renderer/HtmlRendererTest.php @@ -21,7 +21,9 @@ use Yiisoft\ErrorHandler\Tests\Support\TestFriendlyException; use Yiisoft\ErrorHandler\Tests\Support\TestHelper; use Yiisoft\ErrorHandler\Tests\Support\TestInlineCodeDocBlockException; +use Yiisoft\ErrorHandler\Tests\Support\TestOwaspFilterEvasionDocBlockException; use Yiisoft\ErrorHandler\Tests\Support\TestUnsafeDocBlockException; +use Yiisoft\ErrorHandler\Tests\Support\TestUnsafeMarkdownDocBlockException; use function dirname; use function file_exists; @@ -127,8 +129,16 @@ public function testVerboseOutputEscapesUnsafeThrowableDescriptionLinks(): void $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); $result = (string) $errorData; + preg_match('/

(.*?)<\/div>/s', $result, $matches); + $description = $matches[1] ?? ''; $this->assertStringNotContainsString('href="javascript:alert(1)"', $result); + $this->assertNotSame('', $description); + $this->assertStringNotContainsString('assertStringContainsString( + '<img src="x" onerror="alert(1)">', + $description, + ); $this->assertStringContainsString('Click me (javascript:alert(1))', $result); $this->assertStringContainsString( 'Safe link', @@ -136,6 +146,94 @@ public function testVerboseOutputEscapesUnsafeThrowableDescriptionLinks(): void ); } + public function testVerboseOutputEscapesUnsafeThrowableDescriptionMarkdownPayloads(): void + { + $renderer = new HtmlRenderer(); + $exception = new TestUnsafeMarkdownDocBlockException('exception-test-message'); + + $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); + $result = (string) $errorData; + preg_match('/
(.*?)<\/div>/s', $result, $matches); + $description = $matches[1] ?? ''; + + $this->assertNotSame('', $description); + $this->assertStringNotContainsString('href="javascript:alert(document.domain)"', $description); + $this->assertStringNotContainsString('href="javascript:alert(\'html-link\')"', $description); + $this->assertStringNotContainsString('assertStringNotContainsString('assertStringContainsString('Click me (javascript:alert(document.domain))', $description); + $this->assertStringContainsString('!Image payload (javascript:alert('img'))', $description); + $this->assertStringContainsString( + '<a href="javascript:alert('html-link')">Raw HTML link</a>', + $description, + ); + $this->assertStringContainsString( + '<svg onload="alert('svg')"></svg>', + $description, + ); + } + + public function testVerboseOutputEscapesNonHttpSchemesInThrowableDescriptionMarkdownLinks(): void + { + $renderer = new HtmlRenderer(); + $exception = new TestUnsafeMarkdownDocBlockException('exception-test-message'); + + $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); + $result = (string) $errorData; + preg_match('/
(.*?)<\/div>/s', $result, $matches); + $description = $matches[1] ?? ''; + + $this->assertNotSame('', $description); + $this->assertStringContainsString('Encoded payload (JaVaScRiPt:alert(1))', $description); + $this->assertStringContainsString( + 'Data URL (data:text/html,<script>alert(1)</script>)', + $description, + ); + $this->assertStringContainsString( + 'Safe link', + $description, + ); + } + + public function testVerboseOutputEscapesOwaspFilterEvasionThrowableDescriptionPayloads(): void + { + $renderer = new HtmlRenderer(); + $exception = new TestOwaspFilterEvasionDocBlockException('exception-test-message'); + + $errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock()); + $result = (string) $errorData; + preg_match('/
(.*?)<\/div>/s', $result, $matches); + $description = $matches[1] ?? ''; + + $this->assertNotSame('', $description); + $this->assertStringNotContainsString('assertStringContainsString( + '<a href="&#0000106&#0000097&#0000118', + $description, + ); + $this->assertStringContainsString( + '<a href="jav&#x09;ascript:alert('XSS');">Encoded tab payload</a>', + $description, + ); + $this->assertStringContainsString( + '<img src= onmouseover="alert('xss')">', + $description, + ); + $this->assertStringContainsString( + '<img onmouseover="alert('xss')">', + $description, + ); + $this->assertStringContainsString( + '<img dynsrc="javascript:alert('XSS')">', + $description, + ); + $this->assertStringContainsString( + '<img lowsrc="javascript:alert('XSS')">', + $description, + ); + } + public function testVerboseOutputRendersInlineCodeAndSeeTagWithoutLabel(): void { $renderer = new HtmlRenderer(); diff --git a/tests/Support/TestOwaspFilterEvasionDocBlockException.php b/tests/Support/TestOwaspFilterEvasionDocBlockException.php new file mode 100644 index 0000000..84ccfc3 --- /dev/null +++ b/tests/Support/TestOwaspFilterEvasionDocBlockException.php @@ -0,0 +1,24 @@ +Decimal entity payload + * + * Encoded tab payload + * + * + * + * + * + * + * + * + */ +final class TestOwaspFilterEvasionDocBlockException extends RuntimeException {} diff --git a/tests/Support/TestUnsafeMarkdownDocBlockException.php b/tests/Support/TestUnsafeMarkdownDocBlockException.php new file mode 100644 index 0000000..98fff50 --- /dev/null +++ b/tests/Support/TestUnsafeMarkdownDocBlockException.php @@ -0,0 +1,26 @@ +alert(1)) + * + * ![Image payload](javascript:alert('img')) + * + * + * + * Raw HTML link + * + * + */ +final class TestUnsafeMarkdownDocBlockException extends RuntimeException {} From 82446ae58f2547de5cff823364aeb6af796c8751 Mon Sep 17 00:00:00 2001 From: Dmitrii Bukhonov Date: Thu, 12 Mar 2026 18:35:00 +0400 Subject: [PATCH 07/17] Improve verbose error rendering by adding solution and original exception details to templates --- src/Renderer/HtmlRenderer.php | 21 +++++++++++++++++---- templates/development.php | 13 ++++--------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/Renderer/HtmlRenderer.php b/src/Renderer/HtmlRenderer.php index e694efd..5f5e37f 100644 --- a/src/Renderer/HtmlRenderer.php +++ b/src/Renderer/HtmlRenderer.php @@ -212,15 +212,28 @@ public function render(Throwable $t, ?ServerRequestInterface $request = null): E public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData { - $displayThrowable = $t instanceof CompositeException ? $t->getFirstException() : $t; - $exceptionDescription = $displayThrowable instanceof FriendlyExceptionInterface - ? null - : $this->getThrowableDescription($displayThrowable); + $solution = null; + $exceptionDescription = null; + $originalException = $t; + + if ($t instanceof CompositeException) { + $t = $t->getFirstException(); + } + + if ($t instanceof FriendlyExceptionInterface) { + $solution = $t->getSolution(); + } else { + $exceptionDescription = $this->getThrowableDescription($t); + } return new ErrorData( $this->renderTemplate($this->verboseTemplate, [ 'request' => $request, 'throwable' => $t, + 'solution' => $solution, + 'originalException' => $originalException, + 'exceptionClass' => get_class($t), + 'exceptionMessage' => $t->getMessage(), 'exceptionDescription' => $exceptionDescription, ]), [Header::CONTENT_TYPE => self::CONTENT_TYPE], diff --git a/templates/development.php b/templates/development.php index 9ba2087..74f4da4 100644 --- a/templates/development.php +++ b/templates/development.php @@ -1,7 +1,6 @@ getFirstException(); -} -$solution = $throwable instanceof FriendlyExceptionInterface ? $throwable->getSolution() : null; -$exceptionClass = get_class($throwable); -$exceptionMessage = $throwable->getMessage(); - /** * @var HtmlRenderer $this */ From 9e3d0d8a5c30edc828fee03efbce511a403fd012 Mon Sep 17 00:00:00 2001 From: Dmitrii Bukhonov Date: Thu, 12 Mar 2026 18:58:03 +0400 Subject: [PATCH 08/17] Keep original throwable for templates, prepare separate displayThrowable in renderVerbose() --- src/Renderer/HtmlRenderer.php | 16 ++++++++-------- templates/development.php | 20 ++++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Renderer/HtmlRenderer.php b/src/Renderer/HtmlRenderer.php index 5f5e37f..99aef35 100644 --- a/src/Renderer/HtmlRenderer.php +++ b/src/Renderer/HtmlRenderer.php @@ -214,26 +214,26 @@ public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = n { $solution = null; $exceptionDescription = null; - $originalException = $t; + $displayThrowable = $t; if ($t instanceof CompositeException) { - $t = $t->getFirstException(); + $displayThrowable = $t->getFirstException(); } - if ($t instanceof FriendlyExceptionInterface) { - $solution = $t->getSolution(); + if ($displayThrowable instanceof FriendlyExceptionInterface) { + $solution = $displayThrowable->getSolution(); } else { - $exceptionDescription = $this->getThrowableDescription($t); + $exceptionDescription = $this->getThrowableDescription($displayThrowable); } return new ErrorData( $this->renderTemplate($this->verboseTemplate, [ 'request' => $request, 'throwable' => $t, + 'displayThrowable' => $displayThrowable, 'solution' => $solution, - 'originalException' => $originalException, - 'exceptionClass' => get_class($t), - 'exceptionMessage' => $t->getMessage(), + 'exceptionClass' => get_class($displayThrowable), + 'exceptionMessage' => $displayThrowable->getMessage(), 'exceptionDescription' => $exceptionDescription, ]), [Header::CONTENT_TYPE => self::CONTENT_TYPE], diff --git a/templates/development.php b/templates/development.php index 74f4da4..73b316f 100644 --- a/templates/development.php +++ b/templates/development.php @@ -8,8 +8,8 @@ /** * @var ServerRequestInterface|null $request * @var Throwable $throwable + * @var Throwable $displayThrowable * @var string|null $solution - * @var Throwable $originalException * @var string $exceptionClass * @var string $exceptionMessage * @var string|null $exceptionDescription @@ -28,7 +28,7 @@ - <?= $this->htmlEncode($this->getThrowableName($throwable)) ?> + <?= $this->htmlEncode($this->getThrowableName($displayThrowable)) ?>