ForgeErrorHandler

Comprehensive error handling for Forge Kernel. Provides automatic PHP error/exception/shutdown handler registration, debug and production error pages, error logging with rate limiting, and security features for sensitive data masking.

Overview

ForgeErrorHandler is a core module that provides comprehensive error handling for Forge Kernel applications. It automatically registers PHP error, exception, and shutdown handlers to catch and handle all errors gracefully, with different responses for debug and production environments.

Key Features

Automatic PHP handler registration
Debug vs production error pages
Error logging with rate limiting
Security: sensitive data masking
Error fingerprinting for duplicate detection
PSR-3 logger support (optional)
File-based logging fallback
Code snippets with highlighted error lines

What ForgeErrorHandler Provides

  • Automatic Handler Registration: Registers PHP error, exception, and shutdown handlers on initialization
  • Debug Response: Detailed error page with stack trace, code snippets, request data, and environment information (when APP_DEBUG is true)
  • Production Response: User-friendly error page without sensitive information (when APP_DEBUG is false)
  • Error Logging: Logs all errors with comprehensive context (request ID, fingerprint, exception details, memory, duration, source)
  • Rate Limiting: Prevents log spam from duplicate errors (300 seconds default)
  • Error Fingerprinting: Creates unique fingerprints from file, line, and exception class for duplicate detection
  • Security Masking: Automatically masks sensitive data (password, token, secret, authorization, cookie) in logs and error pages
  • Code Snippets: Extracts and displays code context around error lines with highlighting
  • PSR-3 Support: Optional PSR-3 logger injection for production logging
  • File Logging: Fallback file-based logging to storage/logs/errors.log

Core Module: ForgeErrorHandler is a core module (core: true, order: 2), automatically loaded and initialized during Bootstrap. No manual installation is required.

Architecture & Design Philosophy

ForgeErrorHandler uses PHP's native error handling mechanisms to catch all errors, exceptions, and fatal errors. It provides different responses based on the application's debug mode and includes comprehensive logging and security features.

Handler Registration

The module automatically registers three PHP handlers on initialization:

  • Error Handler: Converts PHP errors to exceptions
  • Exception Handler: Handles uncaught exceptions
  • Shutdown Handler: Catches fatal errors that occur during script shutdown

Debug vs Production Mode

The handler detects the application's debug mode via Environment::isDebugEnabled():

  • Debug Mode (APP_DEBUG=true): Shows detailed error page with stack trace, code snippets, request data, and environment information
  • Production Mode (APP_DEBUG=false): Shows user-friendly error page without sensitive information

Error Fingerprinting

Each error is assigned a unique fingerprint based on:

  • File path where error occurred
  • Line number
  • Exception class name

This fingerprint is used for rate limiting to prevent log spam from duplicate errors.

Security by Default

Sensitive data is automatically masked in:

  • Error logs
  • Debug error pages
  • Request parameters (server, query, POST)
  • Session data
  • Environment variables

Installation

ForgeErrorHandler is a core module that is automatically loaded and initialized during Bootstrap. No manual installation is required.

Automatic Initialization

The module is automatically initialized in Bootstrap::setupErrorHandling():

// engine/Core/Bootstrap/Bootstrap.php
if (file_exists(BASE_PATH . "/modules/ForgeErrorHandler/src/ForgeErrorHandler.php")) {
    if (class_exists(\App\Modules\ForgeErrorHandler\Services\ForgeErrorHandlerService::class)) {
        $container->get(\App\Modules\ForgeErrorHandler\Services\ForgeErrorHandlerService::class);
    }
}

Module Configuration

The module is marked as a core module:

#[Module(
    name: 'ForgeErrorHandler',
    version: '0.1.2',
    description: 'An error handler by Forge',
    order: 2,
    core: true  // Core module, automatically loaded
)]

PHP Handler Registration

ForgeErrorHandler automatically registers three PHP handlers to catch all types of errors and exceptions.

Error Handler

Converts PHP errors (warnings, notices, etc.) to exceptions:

set_error_handler([$this, 'phpErrorHandler']);

public function phpErrorHandler(int $severity, string $message, string $file, int $line): bool
{
    if (!(error_reporting() & $severity)) {
        return true;  // Error reporting disabled for this severity
    }
    throw new \ErrorException($message, 0, $severity, $file, $line);
}

This ensures all PHP errors are handled consistently as exceptions.

Exception Handler

Handles all uncaught exceptions:

set_exception_handler([$this, 'phpExceptionHandler']);

public function phpExceptionHandler(Throwable $e): void
{
    try {
        $request = Request::createFromGlobals();
        $this->handle($e, $request)->send();
    } catch (Throwable $fatal) {
        $this->emergencyOutput($fatal);
    } finally {
        exit(1);
    }
}

This is the main entry point for handling exceptions. It logs the error, builds an appropriate response, and exits the script.

Shutdown Handler

Catches fatal errors that occur during script shutdown:

register_shutdown_function([$this, 'phpShutdownHandler']);

public function phpShutdownHandler(): void
{
    $error = error_get_last();
    if ($error === null) {
        return;  // No error occurred
    }
    
    $fatals = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR];
    if (!in_array($error['type'], $fatals, true)) {
        return;  // Not a fatal error
    }
    
    $this->phpExceptionHandler(
        new \ErrorException(
            $error['message'],
            $error['type'],
            0,
            $error['file'],
            $error['line']
        )
    );
}

This catches fatal errors that cannot be caught by the exception handler, such as parse errors or memory exhaustion.

Error Handling Flow

When an error occurs, ForgeErrorHandler follows a consistent flow to log the error and generate an appropriate response.

Error Handling Process

  1. Error Occurs: PHP error, exception, or fatal error is triggered
  2. Handler Catches: Appropriate handler (error, exception, or shutdown) catches the error
  3. Error Fingerprinting: Unique fingerprint is generated from file, line, and exception class
  4. Rate Limit Check: Checks if this error was recently logged (prevents spam)
  5. Context Building: Builds comprehensive context (request ID, exception details, memory, duration, source)
  6. Error Logging: Logs error to PSR-3 logger (if provided) or file (storage/logs/errors.log)
  7. Response Building: Builds debug or production response based on APP_DEBUG
  8. Response Sent: Sends HTTP response and exits

Main Handle Method

public function handle(Throwable $e, Request $request): Response
{
    // Log the error
    $this->logThrowable($e, $request);

    // Return appropriate response based on debug mode
    return $this->debug
        ? $this->buildDebugResponse($e, $request)
        : $this->buildProductionResponse();
}

Debug Response

When APP_DEBUG is true, ForgeErrorHandler displays a detailed error page with comprehensive debugging information.

Debug Error Page Features

  • Exception Details: Type, message, code, file, and line number
  • Stack Trace: Full stack trace with code snippets for each frame
  • Code Snippets: Code context around error lines (5 lines before/after) with highlighted error line
  • Request Information: HTTP method, URI, headers, server parameters (masked)
  • Query Parameters: GET parameters (masked)
  • POST Data: POST parameters (masked)
  • Session Data: All session variables (masked)
  • Environment Variables: All environment variables (masked)

Debug Response Data Structure

$data = [
    'error' => [
        'message' => $e->getMessage(),
        'type'    => get_class($e),
        'code'    => $e->getCode(),
        'file'    => $e->getFile(),
        'line'    => $e->getLine(),
        'trace'   => $trace,  // Filtered trace with code snippets
    ],
    'request' => [
        'method'     => $request->getMethod(),
        'uri'        => $request->getUri(),
        'headers'    => $request->getHeaders(),
        'parameters' => $this->mask($request->serverParams),
        'query'      => $this->mask($request->queryParams),
    ],
    'session'     => $this->mask($_SESSION ?? []),
    'environment' => $this->mask($_ENV),
];

Code Snippets

The debug page extracts code snippets from the stack trace:

private function extractSnippets(Throwable $e): array
{
    $out = [];
    foreach ($e->getTrace() as $frame) {
        $out[] = $this->codeSnippet(
            $frame['file'] ?? $e->getFile(),
            $frame['line'] ?? $e->getLine()
        );
    }
    return $out;
}

private function codeSnippet(string $file, int $line, int $context = 5): array
{
    $real = realpath($file);
    if (!$real || !str_starts_with($real, $this->basePath) || !is_file($real)) {
        return [];
    }
    $lines = file($real, FILE_IGNORE_NEW_LINES);
    $start = max(1, $line - $context);
    $end   = min(count($lines), $line + $context);
    $slice = [];
    for ($i = $start; $i <= $end; $i++) {
        $slice[$i] = $lines[$i - 1];
    }
    return $slice;
}

Code snippets show 5 lines before and after the error line, with the error line highlighted. Only files within BASE_PATH are shown for security.

Production Response

When APP_DEBUG is false, ForgeErrorHandler displays a user-friendly error page without exposing sensitive information.

Production Error Page

The production error page is simple and user-friendly:

private function buildProductionResponse(): Response
{
    return new Response($this->renderUserFriendlyPage(), 500);
}

private function renderUserFriendlyPage(): string
{
    return <<<HTML
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Error</title>
    <style>body{font-family:system-ui,sans-serif;background:#f8fafc;padding:2rem}.box{max-width:600px;margin:2rem auto;padding:2rem;background:#fff;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.1)}</style>
</head>
<body>
    <div class="box">
        <h1>Something went wrong</h1>
        <p>We have been notified. Please try again later.</p>
        <p><a href="/">Go home</a></p>
    </div>
</body>
</html>
HTML;
}

Security: The production error page does not expose any sensitive information, stack traces, or file paths. All errors are still logged for debugging purposes.

Error Logging

ForgeErrorHandler logs all errors with comprehensive context information. It supports both PSR-3 loggers and file-based logging.

Logging Process

  1. Error Fingerprinting: Generate unique fingerprint from file, line, and exception class
  2. Rate Limit Check: Check if this error was recently logged (prevents spam)
  3. Context Building: Build comprehensive context array
  4. Logger Selection: Use PSR-3 logger if provided, otherwise use file logging
  5. Error Logged: Error is logged with full context

Context Information

Each error log includes comprehensive context:

$context = [
    'fingerprint' => $fingerprint,      // Unique error fingerprint
    'request_id'  => $reqId,             // Request ID (from header or generated)
    'exception'   => get_class($e),      // Exception class name
    'code'        => $e->getCode(),      // Exception code
    'file'        => $e->getFile(),      // File where error occurred
    'line'        => $e->getLine(),      // Line number
    'trace'       => $e->getTraceAsString(),  // Full stack trace
    'memory'      => memory_get_peak_usage(true),  // Peak memory usage
    'duration_ms' => round((microtime(true) - $start) * 1000, 2),  // Request duration
    'source'      => $source,            // IP, method, URI (or CLI command)
    'sapi'        => PHP_SAPI,           // Server API (cli, apache2handler, etc.)
    'user_agent'  => $_SERVER['HTTP_USER_AGENT'] ?? '',  // User agent
    'session'     => $this->mask($_SESSION ?? []),  // Session data (masked)
    'get'         => $this->mask($request->queryParams),  // GET params (masked)
    'post'        => $this->mask($request->postData),     // POST params (masked)
];

PSR-3 Logger Support

If a PSR-3 compatible logger is provided via dependency injection, it will be used:

public function __construct(?object $logger = null)
{
    $this->logger = $logger && $this->implementsPsr3($logger) ? $logger : null;
}

private function logThrowable(Throwable $e, Request $request): void
{
    $fingerprint = $this->fingerprint($e);
    $context     = $this->buildContext($e, $request, $fingerprint);

    if ($this->isRateLimited($fingerprint, 300)) {
        return;  // Skip logging if rate limited
    }

    if ($this->logger) {
        $this->logger->error($e->getMessage(), $context);
    } else {
        $this->fileLog($context);
    }
}

File Logging

If no PSR-3 logger is provided, errors are logged to a file:

private function fileLog(array $context): void
{
    $dir = dirname($this->logFile);
    is_dir($dir) || mkdir($dir, 0775, true);
    
    $line = sprintf(
        "[%s] %s [%s] %s – %s:%d | %s\n",
        date('Y-m-d H:i:s'),      // Timestamp
        $context['request_id'],    // Request ID
        $context['fingerprint'],   // Error fingerprint
        $context['exception'],      // Exception class
        $context['file'],          // File path
        $context['line'],          // Line number
        str_replace(["\n", "\r"], ' ', $context['trace'])  // Stack trace (single line)
    );
    
    file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
}

Log file location: storage/logs/errors.log

Log format: [timestamp] request_id [fingerprint] exception – file:line | trace

Security Features

ForgeErrorHandler automatically masks sensitive data to prevent exposure in error logs and debug pages.

Sensitive Data Masking

The following keys are automatically masked (replaced with *****):

  • password
  • token
  • secret
  • authorization
  • cookie

Masking Implementation

private array $hiddenKeys = [
    'password',
    'token',
    'secret',
    'authorization',
    'cookie'
];

private function mask(array $input): array
{
    array_walk_recursive($input, function (&$v, $k) {
        if (is_string($k) && in_array(strtolower($k), $this->hiddenKeys, true)) {
            $v = '*****';
        }
    });
    return $input;
}

Masking is applied recursively to nested arrays and affects:

  • Server parameters
  • Query parameters
  • POST data
  • Session data
  • Environment variables

Customizing Hidden Keys

You can customize the list of hidden keys by modifying the $hiddenKeys property in the service class:

private array $hiddenKeys = [
    'password',
    'token',
    'secret',
    'authorization',
    'cookie',
    'api_key',      // Add custom keys
    'private_key',  // Add custom keys
];

Error Fingerprinting

ForgeErrorHandler creates unique fingerprints for each error to enable duplicate detection and rate limiting.

Fingerprint Generation

The fingerprint is generated from:

  • File path where error occurred
  • Line number
  • Exception class name
private function fingerprint(Throwable $e): string
{
    return substr(
        md5($e->getFile() . ':' . $e->getLine() . ':' . get_class($e)),
        0,
        8
    );
}

The fingerprint is an 8-character MD5 hash, providing a unique identifier for each unique error location and type.

Use Cases

  • Rate Limiting: Prevents logging the same error multiple times within a time window
  • Error Tracking: Enables tracking of specific error patterns
  • Log Analysis: Helps identify frequently occurring errors

Rate Limiting

ForgeErrorHandler implements rate limiting to prevent log spam from duplicate errors.

Rate Limiting Implementation

private array $rateLimitMap = [];

private function isRateLimited(string $fingerprint, int $seconds): bool
{
    $now = time();
    if (isset($this->rateLimitMap[$fingerprint]) 
        && ($now - $this->rateLimitMap[$fingerprint]) < $seconds
    ) {
        return true;  // Rate limited
    }
    $this->rateLimitMap[$fingerprint] = $now;
    return false;  // Not rate limited
}

Default Rate Limit

The default rate limit is 300 seconds (5 minutes). This means the same error (same fingerprint) will only be logged once every 5 minutes.

if ($this->isRateLimited($fingerprint, 300)) {
    return;  // Skip logging if rate limited
}

In-Memory Storage

Rate limiting uses an in-memory map that is reset on each request. This is suitable for preventing log spam within a single request cycle.

Note: For persistent rate limiting across requests, consider implementing a database or cache-based solution.

Code Snippets

ForgeErrorHandler extracts code snippets from the stack trace to provide context around error lines in debug mode.

Code Snippet Extraction

private function codeSnippet(string $file, int $line, int $context = 5): array
{
    $real = realpath($file);
    // Security: Only show files within BASE_PATH
    if (!$real || !str_starts_with($real, $this->basePath) || !is_file($real)) {
        return [];
    }
    
    $lines = file($real, FILE_IGNORE_NEW_LINES);
    if ($lines === false) {
        return [];
    }
    
    $start = max(1, $line - $context);  // 5 lines before
    $end   = min(count($lines), $line + $context);  // 5 lines after
    
    $slice = [];
    for ($i = $start; $i <= $end; $i++) {
        $slice[$i] = $lines[$i - 1];
    }
    return $slice;
}

Security Considerations

  • Only files within BASE_PATH are shown
  • File must exist and be readable
  • Prevents exposure of system files or files outside the application

Context Size

Default context is 5 lines before and after the error line. This provides enough context to understand the error without overwhelming the debug page.

Request Context

ForgeErrorHandler captures comprehensive request context for error logging and debugging.

Request ID

Each request is assigned a unique ID:

$reqId = $_SERVER['HTTP_X_REQUEST_ID'] ?? bin2hex(random_bytes(8));

The request ID is either taken from the X-Request-ID header (if present) or generated as a random 16-character hex string.

Source Information

Source information varies based on SAPI:

$source = PHP_SAPI === 'cli'
    ? ['cli' => implode(' ', $_SERVER['argv'] ?? [])]
    : [
        'ip'     => $this->clientIp(),
        'method' => $request->getMethod(),
        'uri'    => $request->getUri()
    ];

Client IP Detection

Client IP is detected with proxy support:

private function clientIp(): string
{
    return $_SERVER['HTTP_X_FORWARDED_FOR']
        ?? $_SERVER['HTTP_X_REAL_IP']
        ?? $_SERVER['REMOTE_ADDR']
        ?? '0.0.0.0';
}

Additional Context

  • Memory Usage: Peak memory usage during request
  • Duration: Request duration in milliseconds
  • SAPI: Server API (cli, apache2handler, fpm-fcgi, etc.)
  • User Agent: HTTP User-Agent header
  • Session Data: All session variables (masked)
  • GET/POST Data: Query and POST parameters (masked)

Integration Points

ForgeErrorHandler integrates with the Forge Kernel Bootstrap process and can accept optional PSR-3 loggers.

Bootstrap Integration

The error handler is automatically initialized in Bootstrap::setupErrorHandling():

// engine/Core/Bootstrap/Bootstrap.php
public function setupErrorHandling(Container $container): void
{
    ini_set(
        "display_errors",
        Environment::getInstance()->isDevelopment() ? "1" : "0",
    );
    error_reporting(E_ALL);

    if (file_exists(BASE_PATH . "/modules/ForgeErrorHandler/src/ForgeErrorHandler.php")) {
        if (class_exists(\App\Modules\ForgeErrorHandler\Services\ForgeErrorHandlerService::class)) {
            $container->get(\App\Modules\ForgeErrorHandler\Services\ForgeErrorHandlerService::class);
        }
    }
}

PSR-3 Logger Integration

You can inject a PSR-3 compatible logger via dependency injection:

// In your service provider or module
$container->bind(
    \App\Modules\ForgeErrorHandler\Services\ForgeErrorHandlerService::class,
    function (Container $container) {
        $logger = $container->get(Psr\Log\LoggerInterface::class);
        return new \App\Modules\ForgeErrorHandler\Services\ForgeErrorHandlerService($logger);
    }
);

The logger must implement PSR-3 methods: error() and debug().

Module Registration

The module is registered as a core module:

#[Module(
    name: 'ForgeErrorHandler',
    version: '0.1.2',
    description: 'An error handler by Forge',
    order: 2,
    core: true  // Core module, automatically loaded
)]
#[Service]
#[Provides(interface: ForgeErrorHandlerInterface::class, version: '0.1.2')]
final class ForgeErrorHandlerModule
{
    public function register(Container $container): void
    {
        $container->bind(
            ForgeErrorHandlerInterface::class,
            ForgeErrorHandlerService::class
        );
    }
}

Usage Examples

Automatic Error Handling

ForgeErrorHandler automatically catches all errors, exceptions, and fatal errors. No manual configuration is needed:

// This exception will be automatically caught and handled
throw new \Exception('Something went wrong');

// This error will be converted to an exception and handled
trigger_error('This is an error', E_USER_ERROR);

// Fatal errors are also caught
// (e.g., calling undefined function, memory exhaustion, etc.)

Custom Logger Integration

use Psr\Log\LoggerInterface;
use App\Modules\ForgeErrorHandler\Services\ForgeErrorHandlerService;

// In your service provider
$container->bind(ForgeErrorHandlerService::class, function (Container $container) {
    $logger = $container->get(LoggerInterface::class);
    return new ForgeErrorHandlerService($logger);
});

Error Log File Location

Errors are logged to: storage/logs/errors.log

# View recent errors
tail -f storage/logs/errors.log

# Search for specific errors
grep "Exception" storage/logs/errors.log

# Count errors by fingerprint
grep -o "\[.*\]" storage/logs/errors.log | sort | uniq -c

Debug vs Production Behavior

Set APP_DEBUG in your .env file:

# Development - shows detailed error page
APP_DEBUG=true

# Production - shows user-friendly error page
APP_DEBUG=false

Best Practices

Always Enable in Production

  • Always enable ForgeErrorHandler in production (with APP_DEBUG=false)
  • Never disable error handling in production
  • Use production mode to hide sensitive information from users

Monitor Error Logs

  • Regularly monitor storage/logs/errors.log
  • Set up log rotation to prevent log files from growing too large
  • Use log aggregation tools (e.g., ELK, Splunk) for production
  • Set up alerts for critical errors

Use PSR-3 Logger for Production

  • Inject a PSR-3 compatible logger for production environments
  • Use log aggregation services (e.g., Monolog with handlers)
  • Configure log levels appropriately
  • Ensure logs are stored securely and backed up

Review Rate Limiting Settings

  • Adjust rate limiting window (default: 300 seconds) if needed
  • Consider persistent rate limiting for high-traffic applications
  • Monitor rate-limited errors to identify patterns

Customize Hidden Keys

  • Add application-specific sensitive keys to $hiddenKeys
  • Review what data is being logged and ensure sensitive data is masked
  • Test masking in development to ensure it works correctly

Error Page Customization

  • Customize the production error page to match your application's design
  • Update renderUserFriendlyPage() method
  • Consider adding a support email or contact form
  • Ensure the error page is accessible and user-friendly

Security Considerations

  • Never set APP_DEBUG=true in production
  • Review error logs for sensitive information
  • Ensure log files have proper permissions (not world-readable)
  • Use secure log storage for production
  • Regularly rotate and archive log files

Error Handling in Code

  • Use try-catch blocks for expected exceptions
  • Let ForgeErrorHandler handle unexpected exceptions
  • Provide meaningful error messages in your code
  • Use appropriate exception types for different error scenarios