Files
michaelschiemer/docs/migration/ErrorHandling-to-ExceptionHandling-Strategy.md
Michael Schiemer 95147ff23e refactor(deployment): Remove WireGuard VPN dependency and restore public service access
Remove WireGuard integration from production deployment to simplify infrastructure:
- Remove docker-compose-direct-access.yml (VPN-bound services)
- Remove VPN-only middlewares from Grafana, Prometheus, Portainer
- Remove WireGuard middleware definitions from Traefik
- Remove WireGuard IPs (10.8.0.0/24) from Traefik forwarded headers

All monitoring services now publicly accessible via subdomains:
- grafana.michaelschiemer.de (with Grafana native auth)
- prometheus.michaelschiemer.de (with Basic Auth)
- portainer.michaelschiemer.de (with Portainer native auth)

All services use Let's Encrypt SSL certificates via Traefik.
2025-11-05 12:48:25 +01:00

23 KiB

ErrorHandling → ExceptionHandling Migration Strategy

Status: Task 13 Phase 5 - Migration Planning Date: 2025-11-05 Phase 4 Completion: All legacy files examined, incompatibilities documented

Executive Summary

The legacy ErrorHandling module cannot be removed until 5 critical incompatibilities are resolved. This document provides implementation strategies for each blocker.

Critical Blockers

# Blocker Severity Location Impact
1 ErrorAggregator signature mismatch 🔴 CRITICAL ErrorHandler.php:128 Prevents error aggregation
2 ExceptionHandlingMiddleware unreachable code 🔴 URGENT ExceptionHandlingMiddleware.php:32-37 Broken error recovery
3 SecurityEventLogger old types 🔴 HIGH SecurityEventLogger.php:28-52 Breaks DDoS logging
4 Missing CLI error rendering 🔴 HIGH AppBootstrapper.php:155-163 No CLI error handling
5 Missing HTTP Response generation 🔴 HIGH Multiple locations No middleware recovery

Strategy 1: Fix ErrorAggregator Signature Mismatch

Current State (BROKEN)

Location: src/Framework/ErrorHandling/ErrorHandler.php:127-128

// BROKEN: OLD signature call
$this->errorAggregator->processError($errorHandlerContext);

NEW signature requires:

public function processError(
    \Throwable $exception,
    ExceptionContextProvider $contextProvider,
    bool $isDebug = false
): void

Migration Strategy

Option A: Minimal Change (Recommended)

Create adapter method in ErrorHandler that converts ErrorHandlerContext to ExceptionContextProvider:

// Add to ErrorHandler.php
private function dispatchToErrorAggregator(
    \Throwable $exception,
    ErrorHandlerContext $errorHandlerContext
): void {
    // Create ExceptionContextProvider instance
    $contextProvider = $this->container->get(ExceptionContextProvider::class);

    // Convert ErrorHandlerContext to ExceptionContextData
    $contextData = ExceptionContextData::create(
        operation: $errorHandlerContext->exception->operation ?? null,
        component: $errorHandlerContext->exception->component ?? null,
        userId: $errorHandlerContext->request->userId,
        sessionId: $errorHandlerContext->request->sessionId,
        requestId: $errorHandlerContext->request->requestId,
        clientIp: $errorHandlerContext->request->clientIp,
        userAgent: $errorHandlerContext->request->userAgent,
        occurredAt: new \DateTimeImmutable(),
        tags: $errorHandlerContext->exception->tags ?? [],
        metadata: $errorHandlerContext->exception->metadata ?? [],
        data: $errorHandlerContext->metadata
    );

    // Store in WeakMap
    $contextProvider->set($exception, $contextData);

    // Call ErrorAggregator with NEW signature
    $this->errorAggregator->processError(
        $exception,
        $contextProvider,
        $this->isDebugMode()
    );
}

Change at line 127:

// BEFORE (BROKEN)
$this->errorAggregator->processError($errorHandlerContext);

// AFTER (FIXED)
$this->dispatchToErrorAggregator($exception, $errorHandlerContext);

Files to modify:

  • ✏️ src/Framework/ErrorHandling/ErrorHandler.php (add adapter method)

Testing:

  • Trigger error that calls ErrorAggregator
  • Verify context data preserved in WeakMap
  • Check error aggregation dashboard shows correct context

Strategy 2: Fix ExceptionHandlingMiddleware Unreachable Code

Current State (BROKEN)

Location: src/Framework/Http/Middlewares/ExceptionHandlingMiddleware.php:26-39

public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
{
    try {
        return $next($context);
    } catch (\Throwable $e) {
        $error = new ErrorKernel();
        $error->handle($e);  // ← Calls exit() - terminates PHP

        // UNREACHABLE CODE - execution never reaches here
        $response = $this->errorHandler->createHttpResponse($e, $context);
        return $context->withResponse($response);
    }
}

Problem: ErrorKernel.handle() calls exit(), making recovery impossible.

Migration Strategy

Solution: Add non-terminal mode to ErrorKernel

Step 1: Add createHttpResponse() to ErrorKernel

// Add to src/Framework/ExceptionHandling/ErrorKernel.php

/**
 * Create HTTP Response without terminating execution
 * (for middleware recovery pattern)
 */
public function createHttpResponse(\Throwable $exception): Response
{
    // Initialize context if not already done
    if ($this->contextProvider === null) {
        $this->initializeContext($exception);
    }

    // Enrich context from request globals
    $this->enrichContextFromRequest($exception);

    // Create Response using renderer chain
    $response = $this->createResponseFromException($exception);

    // Log error (without terminating)
    $this->logError($exception);

    // Dispatch to aggregator
    $this->dispatchToErrorAggregator($exception);

    return $response;
}

/**
 * Extract response creation from handle()
 */
private function createResponseFromException(\Throwable $exception): Response
{
    // Try framework exception handler
    if ($exception instanceof FrameworkException) {
        return $this->handleFrameworkException($exception);
    }

    // Try specialized handlers
    if ($this->exceptionHandlerManager !== null) {
        $response = $this->exceptionHandlerManager->handle($exception);
        if ($response !== null) {
            return $response;
        }
    }

    // Fallback to renderer chain
    return $this->rendererChain->render($exception, $this->contextProvider);
}

Step 2: Update ExceptionHandlingMiddleware

// Update src/Framework/Http/Middlewares/ExceptionHandlingMiddleware.php

use App\Framework\ExceptionHandling\ErrorKernel;

final readonly class ExceptionHandlingMiddleware
{
    public function __construct(
        private ErrorKernel $errorKernel,  // ← Inject ErrorKernel
        private Logger $logger
    ) {}

    public function __invoke(
        MiddlewareContext $context,
        Next $next,
        RequestStateManager $stateManager
    ): MiddlewareContext {
        try {
            return $next($context);
        } catch (\Throwable $e) {
            // Log error
            $this->logger->error('[Middleware] Exception caught', [
                'exception' => get_class($e),
                'message' => $e->getMessage(),
                'file' => $e->getFile(),
                'line' => $e->getLine()
            ]);

            // Create recovery response (non-terminal)
            $response = $this->errorKernel->createHttpResponse($e);

            // Return context with error response
            return $context->withResponse($response);
        }
    }
}

Files to modify:

  • ✏️ src/Framework/ExceptionHandling/ErrorKernel.php (add createHttpResponse() method)
  • ✏️ src/Framework/Http/Middlewares/ExceptionHandlingMiddleware.php (fix catch block)

Testing:

  • Throw exception in middleware chain
  • Verify Response returned (no exit())
  • Check error logged and aggregated
  • Verify subsequent middleware not executed

Strategy 3: Migrate SecurityEventLogger to ExceptionContextProvider

Current State (OLD Architecture)

Location: src/Framework/ErrorHandling/SecurityEventLogger.php:28-52

public function logSecurityEvent(
    SecurityException $exception,
    ErrorHandlerContext $context  // ← OLD architecture
): void

Dependencies: Used by DDoS system (AdaptiveResponseSystem.php:244-250, 371-379)

Migration Strategy

Solution: Create bridge adapter that converts ExceptionContextProvider to old format

Step 1: Add WeakMap support to SecurityEventLogger

// Update src/Framework/ErrorHandling/SecurityEventLogger.php

use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;

final readonly class SecurityEventLogger
{
    public function __construct(
        private Logger $logger,
        private AppConfig $appConfig,
        private ?ExceptionContextProvider $contextProvider = null  // ← NEW
    ) {}

    /**
     * NEW signature - preferred for new code
     */
    public function logSecurityEventFromException(
        SecurityException $exception
    ): void {
        if ($this->contextProvider === null) {
            throw new \RuntimeException('ExceptionContextProvider required for new logging');
        }

        // Retrieve context from WeakMap
        $exceptionContext = $this->contextProvider->get($exception);

        if ($exceptionContext === null) {
            // Fallback: Create minimal context
            $exceptionContext = ExceptionContextData::create();
        }

        // Convert to OWASP format
        $owaspLog = $this->createOWASPLogFromWeakMap($exception, $exceptionContext);

        // Log via framework logger
        $this->logToFramework($exception, $owaspLog);
    }

    /**
     * LEGACY signature - kept for backward compatibility
     * @deprecated Use logSecurityEventFromException() instead
     */
    public function logSecurityEvent(
        SecurityException $exception,
        ErrorHandlerContext $context
    ): void {
        // Keep existing implementation for backward compatibility
        $owaspLog = $this->createOWASPLog($exception, $context);
        $this->logToFramework($exception, $owaspLog);
    }

    private function createOWASPLogFromWeakMap(
        SecurityException $exception,
        ExceptionContextData $context
    ): array {
        $securityEvent = $exception->getSecurityEvent();

        return [
            'datetime' => date('c'),
            'appid' => $this->appConfig->name,
            'event' => $securityEvent->getEventIdentifier(),
            'level' => $securityEvent->getLogLevel()->value,
            'description' => $securityEvent->getDescription(),
            'useragent' => $context->userAgent,
            'source_ip' => $context->clientIp,
            'host_ip' => $_SERVER['SERVER_ADDR'] ?? 'unknown',
            'hostname' => $_SERVER['SERVER_NAME'] ?? 'unknown',
            'protocol' => $_SERVER['SERVER_PROTOCOL'] ?? 'unknown',
            'port' => $_SERVER['SERVER_PORT'] ?? 'unknown',
            'request_uri' => $_SERVER['REQUEST_URI'] ?? 'unknown',
            'request_method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown',
            'category' => $securityEvent->getCategory(),
            'requires_alert' => $securityEvent->requiresAlert(),
        ];
    }

    private function logToFramework(
        SecurityException $exception,
        array $owaspLog
    ): void {
        $securityEvent = $exception->getSecurityEvent();
        $frameworkLogLevel = $this->mapSecurityLevelToFrameworkLevel(
            $securityEvent->getLogLevel()
        );

        $this->logger->log(
            $frameworkLogLevel,
            $securityEvent->getDescription(),
            [
                'security_event' => $securityEvent->getEventIdentifier(),
                'security_category' => $securityEvent->getCategory(),
                'requires_alert' => $securityEvent->requiresAlert(),
                'owasp_format' => $owaspLog,
            ]
        );
    }
}

Step 2: Update DDoS system to use array logging (no SecurityException needed)

// Update src/Framework/DDoS/Response/AdaptiveResponseSystem.php

// CURRENT (line 244-250):
$this->securityLogger->logSecurityEvent([
    'event_type' => 'ddos_enhanced_monitoring',
    'client_ip' => $assessment->clientIp->value,
    // ...
]);

// AFTER: Keep as-is - this is array-based logging, not SecurityException
// No changes needed here

Files to modify:

  • ✏️ src/Framework/ErrorHandling/SecurityEventLogger.php (add WeakMap support)

Files unchanged:

  • src/Framework/DDoS/Response/AdaptiveResponseSystem.php (already uses array logging)

Testing:

  • Trigger DDoS detection
  • Verify OWASP logs generated
  • Check both old and new signatures work

Strategy 4: Create CLI Error Rendering for ErrorKernel

Current State

Location: src/Framework/Core/AppBootstrapper.php:155-163

private function registerCliErrorHandler(): void
{
    $output = $this->container->has(ConsoleOutput::class)
        ? $this->container->get(ConsoleOutput::class)
        : new ConsoleOutput();

    $cliErrorHandler = new CliErrorHandler($output);  // ← Legacy
    $cliErrorHandler->register();
}

Legacy CliErrorHandler features:

  • Colored console output (ConsoleColor enum)
  • Exit(1) on fatal errors
  • Stack trace formatting

Migration Strategy

Solution: Create CliErrorRenderer for ErrorKernel renderer chain

Step 1: Create CliErrorRenderer

// Create src/Framework/ExceptionHandling/Renderers/CliErrorRenderer.php

namespace App\Framework\ExceptionHandling\Renderers;

use App\Framework\Console\ConsoleOutput;
use App\Framework\Console\ConsoleColor;
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;

final readonly class CliErrorRenderer implements ErrorRenderer
{
    public function __construct(
        private ConsoleOutput $output
    ) {}

    public function canRender(\Throwable $exception): bool
    {
        // Render in CLI context only
        return PHP_SAPI === 'cli';
    }

    public function render(
        \Throwable $exception,
        ?ExceptionContextProvider $contextProvider = null
    ): void {
        $this->output->writeLine(
            "❌ Uncaught " . get_class($exception) . ": " . $exception->getMessage(),
            ConsoleColor::BRIGHT_RED
        );

        $this->output->writeLine(
            "   File: " . $exception->getFile() . ":" . $exception->getLine(),
            ConsoleColor::RED
        );

        if ($exception->getPrevious()) {
            $this->output->writeLine(
                "   Caused by: " . $exception->getPrevious()->getMessage(),
                ConsoleColor::YELLOW
            );
        }

        $this->output->writeLine("   Stack trace:", ConsoleColor::GRAY);
        foreach (explode("\n", $exception->getTraceAsString()) as $line) {
            $this->output->writeLine("     " . $line, ConsoleColor::GRAY);
        }

        // Context information if available
        if ($contextProvider !== null) {
            $context = $contextProvider->get($exception);
            if ($context !== null && $context->operation !== null) {
                $this->output->writeLine(
                    "   Operation: " . $context->operation,
                    ConsoleColor::CYAN
                );
            }
        }
    }
}

Step 2: Register CLI renderer in ErrorKernel

// Update src/Framework/ExceptionHandling/ErrorKernel.php initialization

private function initializeRendererChain(): void
{
    $renderers = [];

    // CLI renderer (highest priority in CLI context)
    if (PHP_SAPI === 'cli' && $this->container->has(ConsoleOutput::class)) {
        $renderers[] = new CliErrorRenderer(
            $this->container->get(ConsoleOutput::class)
        );
    }

    // HTTP renderers
    $renderers[] = new HtmlErrorRenderer($this->container);
    $renderers[] = new JsonErrorRenderer();

    $this->rendererChain = new ErrorRendererChain($renderers);
}

Step 3: Update AppBootstrapper to use ErrorKernel in CLI

// Update src/Framework/Core/AppBootstrapper.php

private function registerCliErrorHandler(): void
{
    // NEW: Use ErrorKernel for CLI (unified architecture)
    new ExceptionHandlerManager();

    // ErrorKernel will detect CLI context and use CliErrorRenderer
    // via its renderer chain
}

Files to modify:

  • ✏️ Create src/Framework/ExceptionHandling/Renderers/CliErrorRenderer.php
  • ✏️ src/Framework/ExceptionHandling/ErrorKernel.php (register CLI renderer)
  • ✏️ src/Framework/Core/AppBootstrapper.php (use ErrorKernel in CLI)

Files to delete (after migration):

  • 🗑️ src/Framework/ErrorHandling/CliErrorHandler.php (replaced by CliErrorRenderer)

Testing:

  • Run console command that throws exception
  • Verify colored output in terminal
  • Check stack trace formatting
  • Verify exit(1) called

Strategy 5: Create HTTP Response Generation for ErrorKernel

Current State

Legacy ErrorHandler.createHttpResponse() pattern (lines 71-86, 115-145) provides:

  • Response generation without terminating
  • ErrorResponseFactory for API/HTML rendering
  • Middleware recovery pattern support

Migration Strategy

Solution: Extract ErrorResponseFactory pattern into ErrorKernel

Step 1: Create ResponseErrorRenderer

// Create src/Framework/ExceptionHandling/Renderers/ResponseErrorRenderer.php

namespace App\Framework\ExceptionHandling\Renderers;

use App\Framework\Http\Response;
use App\Framework\Http\Status;
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
use App\Framework\Template\TemplateRenderer;

final readonly class ResponseErrorRenderer
{
    public function __construct(
        private ?TemplateRenderer $templateRenderer = null
    ) {}

    public function createResponse(
        \Throwable $exception,
        ?ExceptionContextProvider $contextProvider = null
    ): Response {
        // Determine if API or HTML response needed
        $isApiRequest = $this->isApiRequest();

        if ($isApiRequest) {
            return $this->createApiResponse($exception, $contextProvider);
        }

        return $this->createHtmlResponse($exception, $contextProvider);
    }

    private function createApiResponse(
        \Throwable $exception,
        ?ExceptionContextProvider $contextProvider
    ): Response {
        $statusCode = $this->getHttpStatusCode($exception);

        $body = json_encode([
            'error' => [
                'message' => $exception->getMessage(),
                'type' => get_class($exception),
                'code' => $exception->getCode(),
            ]
        ]);

        return new Response(
            status: Status::from($statusCode),
            body: $body,
            headers: ['Content-Type' => 'application/json']
        );
    }

    private function createHtmlResponse(
        \Throwable $exception,
        ?ExceptionContextProvider $contextProvider
    ): Response {
        $statusCode = $this->getHttpStatusCode($exception);

        if ($this->templateRenderer !== null) {
            $body = $this->templateRenderer->render('errors/exception', [
                'exception' => $exception,
                'context' => $contextProvider?->get($exception),
                'statusCode' => $statusCode
            ]);
        } else {
            $body = $this->createFallbackHtml($exception, $statusCode);
        }

        return new Response(
            status: Status::from($statusCode),
            body: $body,
            headers: ['Content-Type' => 'text/html']
        );
    }

    private function isApiRequest(): bool
    {
        // Check Accept header or URL prefix
        $accept = $_SERVER['HTTP_ACCEPT'] ?? '';
        $uri = $_SERVER['REQUEST_URI'] ?? '';

        return str_contains($accept, 'application/json')
            || str_starts_with($uri, '/api/');
    }

    private function getHttpStatusCode(\Throwable $exception): int
    {
        // Map exception types to HTTP status codes
        return match (true) {
            $exception instanceof \InvalidArgumentException => 400,
            $exception instanceof \UnauthorizedException => 401,
            $exception instanceof \ForbiddenException => 403,
            $exception instanceof \NotFoundException => 404,
            default => 500
        };
    }

    private function createFallbackHtml(\Throwable $exception, int $statusCode): string
    {
        return <<<HTML
<!DOCTYPE html>
<html>
<head>
    <title>Error {$statusCode}</title>
</head>
<body>
    <h1>Error {$statusCode}</h1>
    <p>{$exception->getMessage()}</p>
</body>
</html>
HTML;
    }
}

Step 2: Integrate into ErrorKernel.createHttpResponse()

// Update src/Framework/ExceptionHandling/ErrorKernel.php

private ResponseErrorRenderer $responseRenderer;

private function initializeResponseRenderer(): void
{
    $templateRenderer = $this->container->has(TemplateRenderer::class)
        ? $this->container->get(TemplateRenderer::class)
        : null;

    $this->responseRenderer = new ResponseErrorRenderer($templateRenderer);
}

public function createHttpResponse(\Throwable $exception): Response
{
    // Initialize context
    if ($this->contextProvider === null) {
        $this->initializeContext($exception);
    }

    // Enrich from request
    $this->enrichContextFromRequest($exception);

    // Create Response
    $response = $this->responseRenderer->createResponse(
        $exception,
        $this->contextProvider
    );

    // Log error (without terminating)
    $this->logError($exception);

    // Dispatch to aggregator
    $this->dispatchToErrorAggregator($exception);

    return $response;
}

Files to modify:

  • ✏️ Create src/Framework/ExceptionHandling/Renderers/ResponseErrorRenderer.php
  • ✏️ src/Framework/ExceptionHandling/ErrorKernel.php (add createHttpResponse())

Testing:

  • Throw exception in middleware
  • Verify JSON response for /api/* routes
  • Verify HTML response for web routes
  • Check status codes correct

Migration Execution Plan

Phase 5a: Preparation (Current Phase)

  • Document all 5 strategies
  • Review strategies with team
  • Create feature branch: feature/migrate-errorhandling-module

Phase 5b: Implementation Order

Week 1: Foundation

  1. Strategy 5: HTTP Response generation (enables middleware recovery)
  2. Strategy 2: Fix ExceptionHandlingMiddleware (depends on Strategy 5)

Week 2: Compatibility 3. Strategy 1: ErrorAggregator signature fix (critical for logging) 4. Strategy 3: SecurityEventLogger migration (preserves DDoS logging)

Week 3: CLI Support 5. Strategy 4: CLI error rendering (replaces CliErrorHandler)

Week 4: Cleanup 6. Remove legacy ErrorHandling module 7. Update all import statements 8. Run full test suite

Testing Strategy

Per-Strategy Testing:

  • Unit tests for new components
  • Integration tests for error flows
  • Manual testing in development environment

Final Integration Testing:

  • Trigger errors in web context → verify HTTP Response
  • Trigger errors in CLI context → verify colored output
  • Trigger security events → verify OWASP logs
  • Trigger DDoS detection → verify adaptive response
  • Check ErrorAggregator dashboard → verify context preserved

Rollback Plan

Each strategy is independent and can be rolled back:

  • Strategy 1: Remove adapter method
  • Strategy 2: Revert middleware catch block
  • Strategy 3: Remove WeakMap support from SecurityEventLogger
  • Strategy 4: Keep CliErrorHandler active
  • Strategy 5: Don't use createHttpResponse()

Success Criteria

  • All 5 blockers resolved
  • Zero breaking changes to public APIs
  • DDoS system continues functioning
  • CLI error handling preserved
  • Middleware recovery pattern works
  • ErrorAggregator receives correct context
  • All tests passing
  • Legacy ErrorHandling module deleted

Next Actions

Immediate (Phase 5b start):

  1. Create feature branch: git checkout -b feature/migrate-errorhandling-module
  2. Implement Strategy 5 (HTTP Response generation)
  3. Implement Strategy 2 (Fix middleware)
  4. Run tests and verify middleware recovery

This Week:

  • Complete Strategies 1-2
  • Manual testing in development

Next Week:

  • Complete Strategies 3-5
  • Integration testing
  • Code review

Final Week:

  • Remove legacy module
  • Documentation updates
  • Production deployment