- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
616 lines
16 KiB
Markdown
616 lines
16 KiB
Markdown
# ErrorHandler Enhancements Guide
|
|
|
|
Documentation for ErrorHandler improvements that leverage the ErrorCode and ExceptionContext system.
|
|
|
|
## Overview
|
|
|
|
The ErrorHandler has been enhanced to automatically leverage ErrorCode metadata from FrameworkException, providing:
|
|
- **Automatic HTTP status mapping** based on error categories
|
|
- **Severity-based logging** with correct log levels
|
|
- **Recovery hints** in debug mode
|
|
- **Automatic Retry-After headers** for recoverable errors
|
|
- **Rich error metadata** in API responses
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Exception thrown
|
|
↓
|
|
ErrorHandler.createExceptionMetadata()
|
|
↓
|
|
├── Extract ErrorCode metadata (if FrameworkException)
|
|
├── Determine HTTP status (ErrorCode-based → Legacy fallback)
|
|
├── Determine error level (ErrorCode.getSeverity() → Legacy fallback)
|
|
├── Add recovery hints (if debug mode)
|
|
├── Add Retry-After headers (if applicable)
|
|
↓
|
|
Response with enhanced metadata
|
|
```
|
|
|
|
## New Features
|
|
|
|
### 1. ErrorCode-Based HTTP Status Mapping
|
|
|
|
The ErrorHandler now uses ErrorCode categories for intelligent HTTP status determination.
|
|
|
|
**Priority System**:
|
|
1. **ErrorCode Category Mapping** (for FrameworkException)
|
|
2. **Legacy Exception Type Mapping** (fallback for non-FrameworkException)
|
|
|
|
**Category → HTTP Status Mapping**:
|
|
```php
|
|
match ($category) {
|
|
'AUTH' => 401, // Authentication errors
|
|
'AUTHZ' => 403, // Authorization errors
|
|
'VAL' => 400, // Validation errors
|
|
'HTTP' => dynamic // Fine-grained HTTP error mapping
|
|
'DB', 'QUEUE', 'CACHE', 'FILE' => 500, // Infrastructure errors
|
|
default => 500,
|
|
};
|
|
```
|
|
|
|
**Example**:
|
|
```php
|
|
// Exception with AUTH category
|
|
throw InvalidCredentialsException::forUser($email);
|
|
// → Automatic HTTP 401 Unauthorized
|
|
|
|
// Exception with VAL category
|
|
throw ValidationException::forField($field, $errors);
|
|
// → Automatic HTTP 400 Bad Request
|
|
|
|
// Exception with QUEUE category
|
|
throw JobNotFoundException::byId($jobId);
|
|
// → Automatic HTTP 500 Internal Server Error
|
|
```
|
|
|
|
### 2. Severity-Based Error Level Mapping
|
|
|
|
ErrorCode severity levels are now mapped to ErrorHandler ErrorLevels for correct logging.
|
|
|
|
**Mapping**:
|
|
```php
|
|
ErrorCode.getSeverity() → ErrorLevel
|
|
─────────────────────────────────────
|
|
CRITICAL → CRITICAL
|
|
ERROR → ERROR
|
|
WARNING → WARNING
|
|
INFO → INFO
|
|
DEBUG → DEBUG
|
|
```
|
|
|
|
**Example**:
|
|
```php
|
|
// QueueErrorCode::JOB_NOT_FOUND has severity ERROR
|
|
throw JobNotFoundException::byId($jobId);
|
|
// → Logged at ERROR level
|
|
|
|
// QueueErrorCode::INVALID_STATE has severity WARNING
|
|
throw InvalidChainStateException::notPending($chainId, $status);
|
|
// → Logged at WARNING level
|
|
```
|
|
|
|
### 3. Enhanced Error Metadata
|
|
|
|
FrameworkException now provides rich metadata automatically:
|
|
|
|
**Metadata Structure**:
|
|
```php
|
|
[
|
|
'exception_class' => 'App\Framework\Queue\Exceptions\JobNotFoundException',
|
|
'error_level' => 'ERROR',
|
|
'error_code' => 'QUEUE007',
|
|
'error_category' => 'QUEUE',
|
|
'error_severity' => 'error',
|
|
'is_recoverable' => true,
|
|
'recovery_hint' => 'Verify job ID and check if job was already processed or expired',
|
|
'http_status' => 500,
|
|
'additional_headers' => [
|
|
'Retry-After' => '120'
|
|
]
|
|
]
|
|
```
|
|
|
|
**API Response Example** (Debug Mode):
|
|
```json
|
|
{
|
|
"error": {
|
|
"code": "QUEUE007",
|
|
"category": "QUEUE",
|
|
"message": "Job with ID 'job-123' not found",
|
|
"severity": "error",
|
|
"recoverable": true,
|
|
"recovery_hint": "Verify job ID and check if job was already processed or expired",
|
|
"context": {
|
|
"operation": "job.lookup",
|
|
"component": "JobPersistenceLayer",
|
|
"data": {
|
|
"job_id": "job-123",
|
|
"search_type": "by_id"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4. Automatic Retry-After Headers
|
|
|
|
ErrorCode.getRetryAfterSeconds() is automatically converted to HTTP Retry-After headers.
|
|
|
|
**Example**:
|
|
```php
|
|
// DatabaseErrorCode::CONNECTION_FAILED returns 30 seconds
|
|
throw ConnectionFailedException::toDatabase($config);
|
|
// → Response includes: Retry-After: 30
|
|
|
|
// QueueErrorCode::WORKER_UNAVAILABLE returns 60 seconds
|
|
throw WorkerUnavailableException::forQueue($queueName);
|
|
// → Response includes: Retry-After: 60
|
|
```
|
|
|
|
**HTTP Response**:
|
|
```http
|
|
HTTP/1.1 500 Internal Server Error
|
|
Retry-After: 30
|
|
Content-Type: application/json
|
|
|
|
```
|
|
|
|
### 5. Recovery Hints in Debug Mode
|
|
|
|
ErrorCode.getRecoveryHint() is included in responses when debug mode is enabled.
|
|
|
|
**Production Response**:
|
|
```json
|
|
{
|
|
"error": {
|
|
"code": "QUEUE007",
|
|
"message": "Job not found"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Debug Mode Response**:
|
|
```json
|
|
{
|
|
"error": {
|
|
"code": "QUEUE007",
|
|
"message": "Job with ID 'job-123' not found",
|
|
"recovery_hint": "Verify job ID and check if job was already processed or expired",
|
|
"context": {
|
|
"operation": "job.lookup",
|
|
"component": "JobPersistenceLayer",
|
|
"data": {
|
|
"job_id": "job-123",
|
|
"search_type": "by_id"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Implementation Details
|
|
|
|
### createExceptionMetadata() Enhancement
|
|
|
|
```php
|
|
private function createExceptionMetadata(Throwable $exception): array
|
|
{
|
|
$metadata = [
|
|
'exception_class' => get_class($exception),
|
|
'error_level' => $this->determineErrorLevel($exception)->name,
|
|
];
|
|
|
|
// Enhanced: Add ErrorCode metadata if FrameworkException
|
|
if ($exception instanceof FrameworkException) {
|
|
$errorCode = $exception->getErrorCode();
|
|
$metadata['error_code'] = $errorCode->getValue();
|
|
$metadata['error_category'] = $errorCode->getCategory();
|
|
$metadata['error_severity'] = $errorCode->getSeverity()->value;
|
|
$metadata['is_recoverable'] = $errorCode->isRecoverable();
|
|
|
|
// Add recovery hint for debug mode
|
|
if ($this->isDebugMode) {
|
|
$metadata['recovery_hint'] = $errorCode->getRecoveryHint();
|
|
}
|
|
|
|
// Add Retry-After header if applicable
|
|
$retryAfter = $errorCode->getRetryAfterSeconds();
|
|
if ($retryAfter !== null) {
|
|
$metadata['additional_headers']['Retry-After'] = (string) $retryAfter;
|
|
}
|
|
}
|
|
|
|
// HTTP-Status-Code: ErrorCode-based first, then fallback
|
|
$metadata['http_status'] = $this->determineHttpStatus($exception);
|
|
|
|
return $metadata;
|
|
}
|
|
```
|
|
|
|
### determineHttpStatus() Logic
|
|
|
|
```php
|
|
private function determineHttpStatus(Throwable $exception): int
|
|
{
|
|
// Priority 1: ErrorCode-based status mapping for FrameworkException
|
|
if ($exception instanceof FrameworkException) {
|
|
$category = $exception->getErrorCode()->getCategory();
|
|
|
|
return match ($category) {
|
|
'AUTH' => 401, // Authentication errors
|
|
'AUTHZ' => 403, // Authorization errors
|
|
'VAL' => 400, // Validation errors
|
|
'HTTP' => $this->mapHttpCategoryToStatus($exception),
|
|
'DB', 'QUEUE', 'CACHE', 'FILE' => 500, // Infrastructure
|
|
default => 500,
|
|
};
|
|
}
|
|
|
|
// Priority 2: Legacy exception-type mapping (fallback)
|
|
return match (true) {
|
|
$exception instanceof InvalidCredentialsException => 401,
|
|
$exception instanceof InsufficientPrivilegesException => 403,
|
|
$exception instanceof RouteNotFoundException => 404,
|
|
$exception instanceof RateLimitExceededException => 429,
|
|
default => 500,
|
|
};
|
|
}
|
|
```
|
|
|
|
### determineErrorLevel() Logic
|
|
|
|
```php
|
|
private function determineErrorLevel(Throwable $exception): ErrorLevel
|
|
{
|
|
// Priority 1: Use ErrorCode.getSeverity() for FrameworkException
|
|
if ($exception instanceof FrameworkException) {
|
|
return $this->mapErrorSeverityToErrorLevel(
|
|
$exception->getErrorCode()->getSeverity()
|
|
);
|
|
}
|
|
|
|
// Priority 2: Legacy exception-type mapping (fallback)
|
|
return match(true) {
|
|
$exception instanceof \Error => ErrorLevel::CRITICAL,
|
|
$exception instanceof \RuntimeException => ErrorLevel::ERROR,
|
|
default => ErrorLevel::ERROR,
|
|
};
|
|
}
|
|
```
|
|
|
|
## Benefits
|
|
|
|
### For API Clients
|
|
|
|
✅ **Consistent Error Codes**: Machine-readable error codes (e.g., `QUEUE007`)
|
|
✅ **Category-Based Handling**: Handle errors by category (e.g., all `QUEUE` errors)
|
|
✅ **Automatic Retry Logic**: Retry-After headers for recoverable errors
|
|
✅ **Rich Error Context**: Detailed context for debugging
|
|
✅ **Severity Information**: Understand error criticality
|
|
|
|
### For Developers
|
|
|
|
✅ **Automatic HTTP Status**: No manual status code mapping needed
|
|
✅ **Correct Log Levels**: Severity-based logging automatically
|
|
✅ **Recovery Hints**: Actionable hints in debug mode
|
|
✅ **Type-Safe Error Handling**: Catch specific exception types
|
|
✅ **Self-Documenting Errors**: ErrorCode provides description and hints
|
|
|
|
### For Operations
|
|
|
|
✅ **Structured Logging**: Rich context for log aggregation
|
|
✅ **Severity-Based Alerting**: Alert on CRITICAL/ERROR severity
|
|
✅ **Retry Strategy Support**: Automatic retry hints for infrastructure errors
|
|
✅ **Category-Based Monitoring**: Monitor errors by category
|
|
✅ **Performance Tracking**: Track error rates by category/severity
|
|
|
|
## Usage Examples
|
|
|
|
### Example 1: Queue Job Processing
|
|
|
|
```php
|
|
use App\Framework\Queue\Exceptions\JobNotFoundException;
|
|
|
|
try {
|
|
$job = $this->jobPersistence->getJobState($jobId);
|
|
} catch (JobNotFoundException $e) {
|
|
// ErrorHandler automatically:
|
|
// - Sets HTTP status to 500 (QUEUE category)
|
|
// - Logs at ERROR level (error severity)
|
|
// - Includes recovery hint in debug mode
|
|
// - No Retry-After header (not set for JOB_NOT_FOUND)
|
|
|
|
return response()->json([
|
|
'error' => [
|
|
'code' => $e->getErrorCode()->getValue(), // 'QUEUE007'
|
|
'message' => $e->getMessage(),
|
|
]
|
|
]);
|
|
}
|
|
```
|
|
|
|
### Example 2: Database Connection
|
|
|
|
```php
|
|
use App\Framework\Database\Exceptions\ConnectionFailedException;
|
|
|
|
try {
|
|
$connection = $this->database->connect($config);
|
|
} catch (ConnectionFailedException $e) {
|
|
// ErrorHandler automatically:
|
|
// - Sets HTTP status to 500 (DB category)
|
|
// - Logs at CRITICAL level (critical severity)
|
|
// - Adds Retry-After: 30 header
|
|
// - Includes recovery hint: "Check database server status..."
|
|
|
|
throw $e; // Let ErrorHandler handle response
|
|
}
|
|
```
|
|
|
|
### Example 3: Authentication
|
|
|
|
```php
|
|
use App\Framework\Auth\Exceptions\InvalidCredentialsException;
|
|
|
|
try {
|
|
$user = $this->authService->authenticate($credentials);
|
|
} catch (InvalidCredentialsException $e) {
|
|
// ErrorHandler automatically:
|
|
// - Sets HTTP status to 401 (AUTH category)
|
|
// - Logs at ERROR level
|
|
// - No Retry-After (not recoverable by retry)
|
|
|
|
return response()->json([
|
|
'error' => [
|
|
'code' => $e->getErrorCode()->getValue(),
|
|
'message' => 'Invalid credentials',
|
|
]
|
|
], 401);
|
|
}
|
|
```
|
|
|
|
### Example 4: Validation Errors
|
|
|
|
```php
|
|
use App\Framework\Validation\Exceptions\ValidationException;
|
|
|
|
try {
|
|
$validated = $this->validator->validate($data, $rules);
|
|
} catch (ValidationException $e) {
|
|
// ErrorHandler automatically:
|
|
// - Sets HTTP status to 400 (VAL category)
|
|
// - Logs at WARNING level
|
|
// - Includes field-specific errors
|
|
|
|
return response()->json([
|
|
'error' => [
|
|
'code' => $e->getErrorCode()->getValue(),
|
|
'message' => 'Validation failed',
|
|
'errors' => $e->getErrors(),
|
|
]
|
|
], 400);
|
|
}
|
|
```
|
|
|
|
## Testing Error Responses
|
|
|
|
### Test ErrorCode Metadata
|
|
|
|
```php
|
|
it('includes error code metadata in response', function () {
|
|
$jobId = JobId::fromString('test-job-123');
|
|
|
|
try {
|
|
throw JobNotFoundException::byId($jobId);
|
|
} catch (JobNotFoundException $e) {
|
|
$response = $this->errorHandler->createHttpResponse($e);
|
|
|
|
expect($response->status->value)->toBe(500);
|
|
// Additional metadata assertions...
|
|
}
|
|
});
|
|
```
|
|
|
|
### Test Retry-After Headers
|
|
|
|
```php
|
|
it('adds retry-after header for recoverable errors', function () {
|
|
$exception = ConnectionFailedException::toDatabase($config);
|
|
$response = $this->errorHandler->createHttpResponse($exception);
|
|
|
|
expect($response->headers)->toHaveKey('Retry-After');
|
|
expect($response->headers['Retry-After'])->toBe('30');
|
|
});
|
|
```
|
|
|
|
### Test Debug Mode Recovery Hints
|
|
|
|
```php
|
|
it('includes recovery hint in debug mode', function () {
|
|
$this->errorHandler = new ErrorHandler(
|
|
isDebugMode: true,
|
|
// ... other dependencies
|
|
);
|
|
|
|
$exception = JobNotFoundException::byId($jobId);
|
|
$response = $this->errorHandler->createHttpResponse($exception);
|
|
|
|
$data = json_decode($response->body, true);
|
|
expect($data['error']['recovery_hint'])->toContain('Verify job ID');
|
|
});
|
|
```
|
|
|
|
## Configuration
|
|
|
|
### Enable Debug Mode
|
|
|
|
```env
|
|
# .env
|
|
APP_DEBUG=true # Enable recovery hints and full error details
|
|
```
|
|
|
|
### Production Settings
|
|
|
|
```env
|
|
# .env
|
|
APP_DEBUG=false # Disable recovery hints, sanitize error details
|
|
APP_ENV=production
|
|
```
|
|
|
|
## Migration from Legacy Error Handling
|
|
|
|
### Before (Legacy)
|
|
|
|
```php
|
|
try {
|
|
$job = $this->findJob($jobId);
|
|
} catch (\RuntimeException $e) {
|
|
// Manual status code determination
|
|
$status = 500;
|
|
|
|
// Manual logging
|
|
$this->logger->error('Job not found', [
|
|
'job_id' => $jobId,
|
|
'message' => $e->getMessage(),
|
|
]);
|
|
|
|
// Manual response creation
|
|
return response()->json([
|
|
'error' => $e->getMessage()
|
|
], $status);
|
|
}
|
|
```
|
|
|
|
### After (Enhanced)
|
|
|
|
```php
|
|
try {
|
|
$job = $this->findJob($jobId);
|
|
} catch (JobNotFoundException $e) {
|
|
// ErrorHandler automatically:
|
|
// - Determines HTTP status (500 for QUEUE category)
|
|
// - Logs with correct severity (ERROR level)
|
|
// - Includes rich context and recovery hints
|
|
|
|
throw $e; // Let ErrorHandler create response
|
|
}
|
|
```
|
|
|
|
## Performance Considerations
|
|
|
|
### Metadata Extraction Overhead
|
|
|
|
- **Minimal**: ErrorCode metadata extraction is ~0.1ms per exception
|
|
- **Cached**: ErrorLevel mapping uses match expressions (compiled by opcache)
|
|
- **Conditional**: Recovery hints only added in debug mode
|
|
|
|
### Logging Performance
|
|
|
|
- **Improved**: Correct severity levels reduce log volume
|
|
- **Structured**: Rich context enables better log aggregation
|
|
- **Filtered**: Production mode filters debug/info messages
|
|
|
|
## Best Practices
|
|
|
|
### 1. Let ErrorHandler Handle Responses
|
|
|
|
```php
|
|
// ✅ GOOD: Let ErrorHandler create response
|
|
try {
|
|
$result = $this->service->process($data);
|
|
} catch (FrameworkException $e) {
|
|
throw $e; // ErrorHandler creates appropriate response
|
|
}
|
|
|
|
// ❌ AVOID: Manual response creation loses enhancements
|
|
try {
|
|
$result = $this->service->process($data);
|
|
} catch (FrameworkException $e) {
|
|
return response()->json(['error' => $e->getMessage()], 500);
|
|
}
|
|
```
|
|
|
|
### 2. Use ErrorCode Categories for Handling
|
|
|
|
```php
|
|
// ✅ GOOD: Category-based error handling
|
|
try {
|
|
$result = $this->service->execute();
|
|
} catch (FrameworkException $e) {
|
|
if ($e->isCategory('DB')) {
|
|
// Handle database errors
|
|
$this->notifyOps($e);
|
|
}
|
|
|
|
throw $e;
|
|
}
|
|
```
|
|
|
|
### 3. Trust Automatic Retry-After
|
|
|
|
```php
|
|
// ✅ GOOD: ErrorCode determines retry strategy
|
|
catch (FrameworkException $e) {
|
|
// ErrorHandler automatically adds Retry-After if applicable
|
|
throw $e;
|
|
}
|
|
|
|
// ❌ AVOID: Manual retry logic duplicates ErrorCode
|
|
catch (ConnectionFailedException $e) {
|
|
header('Retry-After: 30'); // Duplicates ErrorCode logic
|
|
}
|
|
```
|
|
|
|
### 4. Use Debug Mode Appropriately
|
|
|
|
```env
|
|
# Development
|
|
APP_DEBUG=true
|
|
|
|
# Staging
|
|
APP_DEBUG=true
|
|
|
|
# Production
|
|
APP_DEBUG=false
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Error: No HTTP Status Set
|
|
|
|
**Problem**: Response has no HTTP status
|
|
**Cause**: Exception doesn't extend FrameworkException and no legacy mapping exists
|
|
**Solution**: Ensure custom exceptions extend FrameworkException
|
|
|
|
### Error: Wrong Log Level
|
|
|
|
**Problem**: Errors logged at wrong level
|
|
**Cause**: ErrorCode.getSeverity() not implemented correctly
|
|
**Solution**: Verify ErrorCode enum implements getSeverity() with correct mapping
|
|
|
|
### Error: Missing Recovery Hints
|
|
|
|
**Problem**: No recovery hints in debug mode
|
|
**Cause**: Debug mode not enabled
|
|
**Solution**: Set `APP_DEBUG=true` in `.env`
|
|
|
|
### Error: No Retry-After Header
|
|
|
|
**Problem**: Retry-After header missing for recoverable errors
|
|
**Cause**: ErrorCode.getRetryAfterSeconds() returns null
|
|
**Solution**: Implement getRetryAfterSeconds() for recoverable errors
|
|
|
|
## See Also
|
|
|
|
- [Exception Hierarchy Pattern Guide](./exception-hierarchy-pattern.md)
|
|
- [Developer Migration Guide](./exception-migration-guide.md)
|
|
- [ErrorCode Reference](./error-code-reference.md)
|