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.
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
- Strategy 5: HTTP Response generation (enables middleware recovery)
- 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):
- Create feature branch:
git checkout -b feature/migrate-errorhandling-module - Implement Strategy 5 (HTTP Response generation)
- Implement Strategy 2 (Fix middleware)
- 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