Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
ini-values: zend.exception_ignore_args=Off,zend.exception_string_param_max_len=15
tools: composer

- name: Install composer dependencies
Expand Down
143 changes: 143 additions & 0 deletions src/Pages/InternalErrorPage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php
declare( strict_types = 1 );

namespace DanielWebsite\Pages;

use DanielEScherzer\HTMLBuilder\FluentHTML;
use Throwable;

class InternalErrorPage extends BasePage {

private Throwable $error;

public function __construct( Throwable $error ) {
parent::__construct();
$this->head->append(
FluentHTML::fromTag( 'title' )->addChild( 'Internal Error' )
);
$this->error = $error;
$this->setResponseCode( 500 );
}

protected function build(): void {
$this->addStyleSheet( 'error-styles.css' );
$this->head->append(
FluentHTML::make(
'style',
[],
[
'pre { overflow-x: auto; padding-bottom: 10px; }',
'.error-box { width: fit-content; max-width: 100%; }',
]
)
);
$error = $this->error;
$file = self::formatFile( $error->getFile() );
$this->contentWrapper->append(
FluentHTML::make(
'div',
[ 'class' => 'error-box' ],
[
FluentHTML::make( 'h1', [], 'Internal Error' ),
FluentHTML::make(
'p',
[],
'[' . get_class( $error ) . '] ' . $error->getMessage(),
),
FluentHTML::make(
'p',
[],
[
'From ',
FluentHTML::make( 'code', [], $file ),
' line ',
FluentHTML::make( 'code', [], (string)$error->getLine() ),
]
),
FluentHTML::make( 'p', [], 'Backtrace:' ),
FluentHTML::make(
'pre',
[],
self::formatTrace( $error )
),
]
)
);
}

public static function handleException( Throwable $error ) {
try {
$page = new InternalErrorPage( $error );
$page->getResponse()->applyResponse();
} catch ( Throwable $error2 ) {
self::handleManually( $error, $error2 );
}
}

public static function handleManually( Throwable $error1, Throwable $error2 ) {
http_response_code( 500 );
echo "<!DOCTYPE html>\n";
echo "<html lang='en'>\n";
echo "<head>\n";
echo "<title>Internal Error</title>\n";
echo "<style> .content-wrapper { margin: auto; width: 80%; } </style>\n";
echo "</head>\n<body>\n";
echo "<div class='content-wrapper'>\n";

echo "<h1>Internal Error</h1>\n";
echo '<p>[' . get_class( $error1 ) . '] ' . $error1->getMessage() . "</p>\n";
$file = self::formatFile( $error1->getFile() );
echo "<p>From: <code>$file</code> line " . $error1->getLine() . "</p>\n";
echo "<p>Backtrace:</p>\n";
echo "<pre>" . self::formatTrace( $error1 ) . "</pre>\n";

echo "<p><b>While trying to handle that error, the handler also had an error:</b></p>\n";
echo '<p>[' . get_class( $error2 ) . '] ' . $error2->getMessage() . "</p>\n";
$file = self::formatFile( $error2->getFile() );
echo "<p>From: <code>$file</code> line " . $error2->getLine() . "</p>\n";
echo "<p>Backtrace:</p>\n";
echo "<pre>" . self::formatTrace( $error2 ) . "</pre>\n";

echo "</div></body></html>";
}

/**
* Like Exception::getTraceAsString() but
* - the `/var/www/html/` is removed from the start of files
*/
private static function formatTrace( Throwable $error ): string {
// Don't want to try and reimplement the entirety of the exception
// formatting
$trace = $error->getTraceAsString();
$lines = explode( "\n", $trace );
$betterLines = array_map(
static fn ( $line ) => preg_replace_callback(
"/^(#\d+) ([^\(]+)(\(\d+\): .*$)/",
static fn ( array $matches ): string => $matches[1]
. ' '
. self::formatFile( $matches[2] )
. $matches[3],
$line
),
$lines
);
return implode( "\n", $betterLines );
}

/**
* Nicely format file names, removing common leading prefixes
*/
private static function formatFile( string $file ): string {
// Within docker container
if ( str_starts_with( $file, '/var/www/html/' ) ) {
return '.../' . substr( $file, strlen( '/var/www/html/' ) );
}
// Within GitHub actions
$ghPrefix = '/home/runner/work/website-content/website-content/';
if ( str_starts_with( $file, $ghPrefix ) ) {
return '.../' . substr( $file, strlen( $ghPrefix ) );
}
return $file;
}

}
2 changes: 2 additions & 0 deletions src/setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@

// Autoloading from composer
require_once __DIR__ . '/../vendor/autoload.php';

set_exception_handler( [ \DanielWebsite\Pages\InternalErrorPage::class, 'handleException' ] );
40 changes: 40 additions & 0 deletions tests/StaticOutputTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
use DanielWebsite\Pages\BlogPostPage;
use DanielWebsite\Pages\Error404Page;
use DanielWebsite\Pages\Error405Page;
use DanielWebsite\Pages\InternalErrorPage;
use DanielWebsite\Pages\LandingPage;
use DanielWebsite\Pages\OpenSourcePage;
use DanielWebsite\Pages\RedirectPage;
use DanielWebsite\Pages\ThesisPage;
use DanielWebsite\Pages\ToolPage;
use DanielWebsite\Pages\WorkPage;
use DanielWebsite\Router;
use Exception;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
Expand All @@ -30,6 +32,7 @@
#[CoversClass( BlogPostPage::class )]
#[CoversClass( Error404Page::class )]
#[CoversClass( Error405Page::class )]
#[CoversClass( InternalErrorPage::class )]
#[CoversClass( LandingPage::class )]
#[CoversClass( OpenSourcePage::class )]
#[CoversClass( RedirectPage::class )]
Expand Down Expand Up @@ -98,4 +101,41 @@ public function testRedirect() {
$this->assertInstanceOf( RedirectPage::class, $page );
}

public function testErrorNice() {
ob_start();
InternalErrorPage::handleException( new Exception( 'testing' ) );
$output = ob_get_clean();
// Different include paths in docker and on GitHub
$output = preg_replace(
"/(\/vendor\/bin\/phpunit\(122\):) include\([^\)]+\)/",
"$1 include({path})",
$output
);
$filePath = __DIR__ . '/data/errors-nice.html';
if ( getenv( 'TESTS_UPDATE_EXPECTED' ) === '1' ) {
file_put_contents( $filePath, $output );
}
$this->assertStringEqualsFile( $filePath, $output );
}

public function testErrorManual() {
ob_start();
InternalErrorPage::handleManually(
new Exception( 'first' ),
new Exception( 'second' )
);
$output = ob_get_clean();
// Different include paths in docker and on GitHub
$output = preg_replace(
"/(\/vendor\/bin\/phpunit\(122\):) include\([^\)]+\)/",
"$1 include({path})",
$output
);
$filePath = __DIR__ . '/data/errors-manual.html';
if ( getenv( 'TESTS_UPDATE_EXPECTED' ) === '1' ) {
file_put_contents( $filePath, $output );
}
$this->assertStringEqualsFile( $filePath, $output );
}

}
41 changes: 41 additions & 0 deletions tests/data/errors-manual.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<title>Internal Error</title>
<style> .content-wrapper { margin: auto; width: 80%; } </style>
</head>
<body>
<div class='content-wrapper'>
<h1>Internal Error</h1>
<p>[Exception] first</p>
<p>From: <code>.../tests/StaticOutputTest.php</code> line 124</p>
<p>Backtrace:</p>
<pre>#0 .../vendor/phpunit/phpunit/src/Framework/TestCase.php(1317): DanielWebsite\Tests\StaticOutputTest->testErrorManual()
#1 .../vendor/phpunit/phpunit/src/Framework/TestCase.php(517): PHPUnit\Framework\TestCase->runTest()
#2 .../vendor/phpunit/phpunit/src/Framework/TestRunner/TestRunner.php(99): PHPUnit\Framework\TestCase->runBare()
#3 .../vendor/phpunit/phpunit/src/Framework/TestCase.php(357): PHPUnit\Framework\TestRunner->run(Object(DanielWebsite\Tests\StaticOutputTest))
#4 .../vendor/phpunit/phpunit/src/Framework/TestSuite.php(374): PHPUnit\Framework\TestCase->run()
#5 .../vendor/phpunit/phpunit/src/Framework/TestSuite.php(374): PHPUnit\Framework\TestSuite->run()
#6 .../vendor/phpunit/phpunit/src/Framework/TestSuite.php(374): PHPUnit\Framework\TestSuite->run()
#7 .../vendor/phpunit/phpunit/src/TextUI/TestRunner.php(64): PHPUnit\Framework\TestSuite->run()
#8 .../vendor/phpunit/phpunit/src/TextUI/Application.php(229): PHPUnit\TextUI\TestRunner->run(Object(PHPUnit\TextUI\Configuration\Configuration), Object(PHPUnit\Runner\ResultCache\DefaultResultCache), Object(PHPUnit\Framework\TestSuite))
#9 .../vendor/phpunit/phpunit/phpunit(104): PHPUnit\TextUI\Application->run(Array)
#10 .../vendor/bin/phpunit(122): include({path})
#11 {main}</pre>
<p><b>While trying to handle that error, the handler also had an error:</b></p>
<p>[Exception] second</p>
<p>From: <code>.../tests/StaticOutputTest.php</code> line 125</p>
<p>Backtrace:</p>
<pre>#0 .../vendor/phpunit/phpunit/src/Framework/TestCase.php(1317): DanielWebsite\Tests\StaticOutputTest->testErrorManual()
#1 .../vendor/phpunit/phpunit/src/Framework/TestCase.php(517): PHPUnit\Framework\TestCase->runTest()
#2 .../vendor/phpunit/phpunit/src/Framework/TestRunner/TestRunner.php(99): PHPUnit\Framework\TestCase->runBare()
#3 .../vendor/phpunit/phpunit/src/Framework/TestCase.php(357): PHPUnit\Framework\TestRunner->run(Object(DanielWebsite\Tests\StaticOutputTest))
#4 .../vendor/phpunit/phpunit/src/Framework/TestSuite.php(374): PHPUnit\Framework\TestCase->run()
#5 .../vendor/phpunit/phpunit/src/Framework/TestSuite.php(374): PHPUnit\Framework\TestSuite->run()
#6 .../vendor/phpunit/phpunit/src/Framework/TestSuite.php(374): PHPUnit\Framework\TestSuite->run()
#7 .../vendor/phpunit/phpunit/src/TextUI/TestRunner.php(64): PHPUnit\Framework\TestSuite->run()
#8 .../vendor/phpunit/phpunit/src/TextUI/Application.php(229): PHPUnit\TextUI\TestRunner->run(Object(PHPUnit\TextUI\Configuration\Configuration), Object(PHPUnit\Runner\ResultCache\DefaultResultCache), Object(PHPUnit\Framework\TestSuite))
#9 .../vendor/phpunit/phpunit/phpunit(104): PHPUnit\TextUI\Application->run(Array)
#10 .../vendor/bin/phpunit(122): include({path})
#11 {main}</pre>
</div></body></html>
13 changes: 13 additions & 0 deletions tests/data/errors-nice.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en"><head><link rel="icon" href="data:,"><meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"><link rel="stylesheet" type="text/css" href="/resources/default-styles.css"><title>Internal Error</title><link rel="stylesheet" type="text/css" href="/resources/error-styles.css"><style>pre { overflow-x: auto; padding-bottom: 10px; }.error-box { width: fit-content; max-width: 100%; }</style></head><body><div class="des-navbar"><a href="/Home">Home</a><a href="/files/Resume.pdf">Résumé</a><a href="/OpenSource">Open Source</a><a href="/Work">Work</a><a href="/Blog">Blog</a></div><div class="content-wrapper"><div class="error-box"><h1>Internal Error</h1><p>[Exception] testing</p><p>From <code>.../tests/StaticOutputTest.php</code> line <code>106</code></p><p>Backtrace:</p><pre>#0 .../vendor/phpunit/phpunit/src/Framework/TestCase.php(1317): DanielWebsite\Tests\StaticOutputTest-&gt;testErrorNice()
#1 .../vendor/phpunit/phpunit/src/Framework/TestCase.php(517): PHPUnit\Framework\TestCase-&gt;runTest()
#2 .../vendor/phpunit/phpunit/src/Framework/TestRunner/TestRunner.php(99): PHPUnit\Framework\TestCase-&gt;runBare()
#3 .../vendor/phpunit/phpunit/src/Framework/TestCase.php(357): PHPUnit\Framework\TestRunner-&gt;run(Object(DanielWebsite\Tests\StaticOutputTest))
#4 .../vendor/phpunit/phpunit/src/Framework/TestSuite.php(374): PHPUnit\Framework\TestCase-&gt;run()
#5 .../vendor/phpunit/phpunit/src/Framework/TestSuite.php(374): PHPUnit\Framework\TestSuite-&gt;run()
#6 .../vendor/phpunit/phpunit/src/Framework/TestSuite.php(374): PHPUnit\Framework\TestSuite-&gt;run()
#7 .../vendor/phpunit/phpunit/src/TextUI/TestRunner.php(64): PHPUnit\Framework\TestSuite-&gt;run()
#8 .../vendor/phpunit/phpunit/src/TextUI/Application.php(229): PHPUnit\TextUI\TestRunner-&gt;run(Object(PHPUnit\TextUI\Configuration\Configuration), Object(PHPUnit\Runner\ResultCache\DefaultResultCache), Object(PHPUnit\Framework\TestSuite))
#9 .../vendor/phpunit/phpunit/phpunit(104): PHPUnit\TextUI\Application-&gt;run(Array)
#10 .../vendor/bin/phpunit(122): include({path})
#11 {main}</pre></div></div><div class="des-footer"><div class="des-footer--content">Content is © 2025 Daniel Scherzer</div></div></body></html>