- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
16 KiB
Error Handling & Debugging
This guide covers error handling patterns and debugging strategies in the framework.
Exception Handling
All custom exceptions in the framework must extend FrameworkException to ensure consistent error handling, logging, and recovery mechanisms.
The FrameworkException System
The framework provides a sophisticated exception system with:
- ExceptionContext: Rich context information for debugging
- ErrorCode: Categorized error codes with recovery hints
- RetryAfter: Support for recoverable operations
- Fluent Interface: Easy context building
Creating Custom Exceptions
namespace App\Domain\User\Exceptions;
use App\Framework\Exception\FrameworkException;
use App\Framework\Exception\Core\DatabaseErrorCode;
use App\Framework\Exception\ExceptionContext;
final class UserNotFoundException extends FrameworkException
{
public static function byId(UserId $id): self
{
return self::create(
DatabaseErrorCode::ENTITY_NOT_FOUND,
"User with ID '{$id->toString()}' not found"
)->withData([
'user_id' => $id->toString(),
'search_type' => 'by_id'
]);
}
public static function byEmail(Email $email): self
{
$context = ExceptionContext::forOperation('user.lookup', 'UserRepository')
->withData(['email' => $email->getMasked()]);
return self::fromContext(
"User with email not found",
$context,
DatabaseErrorCode::ENTITY_NOT_FOUND
);
}
}
Using ErrorCode Enums
The framework provides category-specific error code enums for better organization and type safety:
use App\Framework\Exception\Core\DatabaseErrorCode;
use App\Framework\Exception\Core\AuthErrorCode;
use App\Framework\Exception\Core\HttpErrorCode;
use App\Framework\Exception\Core\SecurityErrorCode;
use App\Framework\Exception\Core\ValidationErrorCode;
// Database errors
DatabaseErrorCode::CONNECTION_FAILED
DatabaseErrorCode::QUERY_FAILED
DatabaseErrorCode::TRANSACTION_FAILED
DatabaseErrorCode::CONSTRAINT_VIOLATION
// Authentication errors
AuthErrorCode::CREDENTIALS_INVALID
AuthErrorCode::TOKEN_EXPIRED
AuthErrorCode::SESSION_EXPIRED
AuthErrorCode::ACCOUNT_LOCKED
// HTTP errors
HttpErrorCode::BAD_REQUEST
HttpErrorCode::NOT_FOUND
HttpErrorCode::METHOD_NOT_ALLOWED
HttpErrorCode::RATE_LIMIT_EXCEEDED
// Security errors
SecurityErrorCode::CSRF_TOKEN_INVALID
SecurityErrorCode::SQL_INJECTION_DETECTED
SecurityErrorCode::XSS_DETECTED
SecurityErrorCode::PATH_TRAVERSAL_DETECTED
// Validation errors
ValidationErrorCode::INVALID_INPUT
ValidationErrorCode::REQUIRED_FIELD_MISSING
ValidationErrorCode::BUSINESS_RULE_VIOLATION
ValidationErrorCode::INVALID_FORMAT
// Using error codes in exceptions:
throw FrameworkException::create(
DatabaseErrorCode::QUERY_FAILED,
"Failed to execute user query"
)->withContext(
ExceptionContext::forOperation('user.find', 'UserRepository')
->withData(['query' => 'SELECT * FROM users WHERE id = ?'])
->withDebug(['bind_params' => [$userId]])
);
Exception Context Building
// Method 1: Using factory methods
$exception = FrameworkException::forOperation(
'payment.process',
'PaymentService',
'Payment processing failed',
HttpErrorCode::BAD_GATEWAY
)->withData([
'amount' => $amount->toArray(),
'gateway' => 'stripe',
'customer_id' => $customerId
])->withMetadata([
'attempt' => 1,
'idempotency_key' => $idempotencyKey
]);
// Method 2: Building context separately
$context = ExceptionContext::empty()
->withOperation('order.validate', 'OrderService')
->withData([
'order_id' => $orderId,
'total' => $total->toDecimal()
])
->withDebug([
'validation_rules' => ['min_amount', 'max_items'],
'failed_rule' => 'min_amount'
]);
throw FrameworkException::fromContext(
'Order validation failed',
$context,
ValidationErrorCode::BUSINESS_RULE_VIOLATION
);
Recoverable Exceptions
// Creating recoverable exceptions with retry hints
final class RateLimitException extends FrameworkException
{
public static function exceeded(int $retryAfter): self
{
return self::create(
HttpErrorCode::RATE_LIMIT_EXCEEDED,
'API rate limit exceeded'
)->withRetryAfter($retryAfter)
->withData(['retry_after_seconds' => $retryAfter]);
}
}
// Using in code
try {
$response = $apiClient->request($endpoint);
} catch (RateLimitException $e) {
if ($e->isRecoverable()) {
$waitTime = $e->getRetryAfter();
// Schedule retry after $waitTime seconds
}
throw $e;
}
Exception Categories
// Check exception category for handling strategies
try {
$result = $operation->execute();
} catch (FrameworkException $e) {
if ($e->isCategory('AUTH')) {
// Handle authentication errors
return $this->redirectToLogin();
}
if ($e->isCategory('VAL')) {
// Handle validation errors
return $this->validationErrorResponse($e);
}
if ($e->isErrorCode(DatabaseErrorCode::CONNECTION_FAILED)) {
// Handle specific database connection errors
$this->notifyOps($e);
}
throw $e;
}
Simple Exceptions for Quick Use
// When you don't need the full context system
throw FrameworkException::simple('Quick error message');
// With previous exception
} catch (\PDOException $e) {
throw FrameworkException::simple(
'Database operation failed',
$e,
500
);
}
Exception Data Sanitization
The framework automatically sanitizes sensitive data in exceptions:
// Sensitive keys are automatically redacted
$exception->withData([
'username' => 'john@example.com',
'password' => 'secret123', // Will be logged as '[REDACTED]'
'api_key' => 'sk_live_...' // Will be logged as '[REDACTED]'
]);
Best Practices
- Always extend FrameworkException for custom exceptions
- Use ErrorCode enum for categorizable errors
- Provide rich context with operation, component, and data
- Use factory methods for consistent exception creation
- Sanitize sensitive data (automatic for common keys)
- Make exceptions domain-specific (UserNotFoundException vs generic NotFoundException)
- Include recovery hints for recoverable errors
Unified Error Kernel Architecture
The framework uses a context-aware error handling system centered around the ErrorKernel class that automatically detects execution context (CLI vs HTTP) and handles errors accordingly.
ErrorKernel Overview
Location: src/Framework/ExceptionHandling/ErrorKernel.php
Key Features:
- Automatic context detection (CLI vs HTTP)
- Colored console output for CLI errors
- HTTP Response objects for web errors
- Integration with OWASP Security Event System
- Unified error logging via LogReporter
use App\Framework\ExceptionHandling\ErrorKernel;
final readonly class ErrorKernel
{
public function __construct(
private ErrorRendererFactory $rendererFactory = new ErrorRendererFactory,
private ?ExecutionContext $executionContext = null,
private ?ConsoleOutput $consoleOutput = null
) {}
/**
* Context-aware exception handler
* - CLI: Colored console output
* - HTTP: Logs error (middleware creates response)
*/
public function handle(Throwable $e, array $context = []): mixed
{
// Automatic logging
$log = new LogReporter();
$log->report($e);
// Context-aware handling
$executionContext = $this->executionContext ?? ExecutionContext::detect();
if ($executionContext->isCli()) {
$this->handleCliException($e);
return null;
}
// HTTP context - middleware will create response
return null;
}
/**
* Create HTTP Response from exception (for middleware recovery)
*/
public function createHttpResponse(
Throwable $exception,
?ExceptionContextProvider $contextProvider = null,
bool $isDebugMode = false
): Response {
$renderer = new ResponseErrorRenderer($isDebugMode);
return $renderer->createResponse($exception, $contextProvider);
}
}
CLI Error Handling
CliErrorHandler registers global PHP error handlers for CLI context:
use App\Framework\ExceptionHandling\CliErrorHandler;
use App\Framework\Console\ConsoleOutput;
// Registration in AppBootstrapper
$output = new ConsoleOutput();
$cliErrorHandler = new CliErrorHandler($output);
$cliErrorHandler->register();
// Automatic colored output for errors:
// - Red for uncaught exceptions
// - Yellow for warnings
// - Cyan for notices
// - Full stack traces in CLI
HTTP Error Handling
ExceptionHandlingMiddleware catches exceptions in HTTP request pipeline:
use App\Framework\Http\Middlewares\ExceptionHandlingMiddleware;
#[MiddlewarePriorityAttribute(MiddlewarePriority::ERROR_HANDLING)]
final readonly class ExceptionHandlingMiddleware implements HttpMiddleware
{
public function __invoke(
MiddlewareContext $context,
Next $next,
RequestStateManager $stateManager
): MiddlewareContext {
try {
return $next($context);
} catch (\Throwable $e) {
// Log exception
$this->logger->error('Unhandled exception in HTTP request', [
'exception' => get_class($e),
'message' => $e->getMessage(),
]);
// Create HTTP Response
$errorKernel = new ErrorKernel();
$response = $errorKernel->createHttpResponse(
$e,
null,
isDebugMode: false
);
return $context->withResponse($response);
}
}
}
OWASP Security Event Integration
Exceptions can trigger OWASP security events for audit logging:
use App\Application\Security\OWASPSecurityEventLogger;
use App\Application\Security\OWASPEventIdentifier;
// Automatic security logging
try {
$this->authenticateUser($credentials);
} catch (AuthenticationException $e) {
// ErrorKernel logs exception
$this->errorKernel->handle($e);
// OWASP event for security audit trail
$this->eventDispatcher->dispatch(
new AuthenticationFailedEvent(
OWASPEventIdentifier::AUTHN_LOGIN_FAILURE,
$credentials->username,
$e->getMessage()
)
);
throw $e;
}
Legacy ErrorHandling Module Removed
IMPORTANT: The legacy ErrorHandling module (src/Framework/ErrorHandling/) has been completely removed as of the unified exception architecture migration.
Migration Path:
- All error handling now uses
ErrorKernelandFrameworkException - CLI errors:
CliErrorHandler→ErrorKernel - HTTP errors:
ExceptionHandlingMiddleware→ErrorKernel - Security events: Direct event dispatch via
EventDispatcher
Old Pattern (removed):
// ❌ Legacy - NO LONGER EXISTS
use App\Framework\ErrorHandling\ErrorHandler;
use App\Framework\ErrorHandling\SecurityEventLogger;
$errorHandler = new ErrorHandler();
$errorHandler->register();
New Pattern (current):
// ✅ Unified - ErrorKernel
use App\Framework\ExceptionHandling\ErrorKernel;
$errorKernel = new ErrorKernel();
$errorKernel->handle($exception);
Logging Best Practices
Automatic Exception Logging
All exceptions handled by ErrorKernel are automatically logged via LogReporter:
// Automatic logging happens in ErrorKernel::handle()
$log = new LogReporter();
$log->report($exception);
// Logs include:
// - Exception class and message
// - Stack trace
// - File and line number
// - Context data
Manual Logging
use App\Framework\Logging\Logger;
// Log exceptions with context
try {
$user = $this->userRepository->find($userId);
} catch (UserNotFoundException $e) {
Logger::error('User lookup failed', [
'user_id' => $userId,
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
throw $e;
}
// Log levels
Logger::debug('Debugging information');
Logger::info('Informational message');
Logger::warning('Warning condition');
Logger::error('Error condition');
Logger::critical('Critical failure');
Debug Strategies
Development vs Production
Development (APP_DEBUG=true):
- Full stack traces displayed
- Detailed error messages
- Debug data in responses
- SQL query logging
Production (APP_DEBUG=false):
- Generic error messages
- Stack traces hidden from users
- Errors logged server-side
- Security-safe responses
Debugging Tools
// Enable debug mode in ErrorKernel
$errorKernel = new ErrorKernel(
executionContext: ExecutionContext::cli(),
consoleOutput: new ConsoleOutput()
);
// HTTP Response with debug mode
$response = $errorKernel->createHttpResponse(
$exception,
$contextProvider,
isDebugMode: true // Shows stack trace in response
);
Error Context Providers
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
// Attach custom context to exceptions
$contextProvider = new ExceptionContextProvider();
$contextProvider->attachContext($exception, [
'request_id' => $requestId,
'user_id' => $userId,
'operation' => 'payment.process'
]);
$response = $errorKernel->createHttpResponse(
$exception,
$contextProvider,
isDebugMode: false
);
Error Recovery Patterns
Graceful Degradation
// Try primary service, fallback to secondary
try {
return $this->primaryCache->get($key);
} catch (CacheException $e) {
Logger::warning('Primary cache failed, using fallback', [
'error' => $e->getMessage()
]);
return $this->fallbackCache->get($key);
}
Circuit Breaker Pattern
use App\Framework\Resilience\CircuitBreaker;
$circuitBreaker = new CircuitBreaker(
failureThreshold: 5,
timeout: Duration::fromSeconds(60)
);
try {
return $circuitBreaker->call(function() {
return $this->externalApi->request($endpoint);
});
} catch (CircuitOpenException $e) {
// Circuit is open - use cached response
return $this->cachedResponse;
}
Retry with Exponential Backoff
use App\Framework\Queue\ValueObjects\RetryStrategy;
$retryStrategy = new ExponentialBackoffStrategy(
maxAttempts: 3,
baseDelaySeconds: 60
);
$attempt = 0;
while ($attempt < $retryStrategy->getMaxAttempts()) {
try {
return $this->performOperation();
} catch (TransientException $e) {
$attempt++;
if (!$retryStrategy->shouldRetry($attempt)) {
throw $e;
}
$delay = $retryStrategy->getDelay($attempt);
sleep($delay->toSeconds());
}
}
Common Error Scenarios
1. Database Connection Failure
try {
$connection = $this->connectionPool->getConnection();
} catch (ConnectionException $e) {
// Log error
$this->errorKernel->handle($e);
// Return cached data or error response
return $this->getCachedData() ?? $this->errorResponse();
}
2. Validation Errors
try {
$user = User::create($email, $name);
} catch (ValidationException $e) {
// Return validation errors to user
return new JsonResult([
'errors' => $e->getErrors()
], status: Status::UNPROCESSABLE_ENTITY);
}
3. Authentication Failures
try {
$user = $this->authenticator->authenticate($credentials);
} catch (AuthenticationException $e) {
// Log security event
$this->eventDispatcher->dispatch(
new AuthenticationFailedEvent($credentials->username)
);
// Return 401 Unauthorized
return new JsonResult([
'error' => 'Invalid credentials'
], status: Status::UNAUTHORIZED);
}
4. Resource Not Found
try {
$order = $this->orderRepository->find($orderId);
} catch (OrderNotFoundException $e) {
// Return 404 Not Found
return new JsonResult([
'error' => 'Order not found'
], status: Status::NOT_FOUND);
}
5. Rate Limit Exceeded
try {
$this->rateLimiter->checkLimit($userId);
} catch (RateLimitException $e) {
// Return 429 Too Many Requests with retry hint
return new JsonResult([
'error' => 'Rate limit exceeded',
'retry_after' => $e->getRetryAfter()
], status: Status::TOO_MANY_REQUESTS);
}