Files
michaelschiemer/docs/guides/troubleshooting.md
Michael Schiemer 36ef2a1e2c
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
fix: Gitea Traefik routing and connection pool optimization
- 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
2025-11-09 14:46:15 +01:00

1075 lines
22 KiB
Markdown

# Troubleshooting
Comprehensive guide for diagnosing and fixing common issues in the Custom PHP Framework.
## Common Errors
### Class Not Found Errors
**Symptom**: `Class 'App\...' not found`
**Causes**:
- Autoloader not regenerated after creating new classes
- Incorrect namespace
- File not saved in correct directory
**Solutions**:
```bash
# Regenerate autoloader
composer reload
# Or in Docker
make reload
# Check namespace matches directory structure
# src/Domain/User/Services/UserService.php
namespace App\Domain\User\Services;
```
**Prevention**:
- Always run `composer reload` after creating new classes
- Use PSR-4 autoloading standards
- Namespace must match directory structure
---
### Container Binding Missing
**Symptom**: `No binding found for interface X`
**Causes**:
- Missing `#[Initializer]` attribute
- Initializer not discovered (cache issue)
- Circular dependency
**Solutions**:
```php
// 1. Add Initializer attribute
final readonly class ServiceInitializer
{
#[Initializer]
public function initialize(Container $container): void
{
$container->singleton(
UserRepository::class,
new DatabaseUserRepository($container->get(Connection::class))
);
}
}
// 2. Clear discovery cache
rm -rf storage/cache/discovery_*
// 3. Check for circular dependencies in constructor
// ❌ Bad: A depends on B, B depends on A
final readonly class ServiceA
{
public function __construct(private readonly ServiceB $b) {}
}
final readonly class ServiceB
{
public function __construct(private readonly ServiceA $a) {} // Circular!
}
```
**Prevention**:
- Use `#[Initializer]` for all service registrations
- Avoid circular dependencies through interface abstraction
- Use method injection instead of constructor injection when needed
---
### Route Not Found (404)
**Symptom**: Route returns 404 even though controller exists
**Causes**:
- Missing `#[Route]` attribute
- Route cache out of date
- HTTP method mismatch
- Middleware blocking request
**Solutions**:
```bash
# 1. Check route is registered
docker exec php php console.php routes:list
# 2. Clear route cache
rm -rf storage/cache/routes_*
# 3. Verify Route attribute
#[Route(path: '/api/users', method: Method::GET)]
public function getUsers(): JsonResult
# 4. Check middleware chain
docker exec php php -r "
use App\Framework\Discovery\UnifiedDiscoveryService;
\$discovery = new UnifiedDiscoveryService();
\$routes = \$discovery->discoverRoutes();
print_r(\$routes);
"
```
**Common Mistakes**:
```php
// ❌ Wrong: Method mismatch
#[Route(path: '/api/users', method: Method::GET)]
// Request: POST /api/users → 404
// ✅ Correct: Match HTTP method
#[Route(path: '/api/users', method: Method::POST)]
```
---
### HTTPS Required Error
**Symptom**: `HTTPS is required for this operation`
**Causes**:
- Accessing framework via HTTP instead of HTTPS
- Missing SSL certificates in development
- Reverse proxy not forwarding HTTPS headers
**Solutions**:
```bash
# Development: Use HTTPS
https://localhost/ # ✅ Correct
http://localhost/ # ❌ Wrong - will be rejected
# Check SSL certificates exist
ls -la docker/ssl/
# Should see: localhost.crt, localhost.key
# Regenerate if missing
cd docker/ssl
./generate-ssl.sh
```
**Production Fix**:
```nginx
# Nginx: Forward HTTPS headers
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Ssl on;
```
---
### User Agent Required Error
**Symptom**: `Valid User-Agent header is required`
**Causes**:
- Missing User-Agent in cURL/API requests
- Bot/Scanner detection blocking legitimate requests
**Solutions**:
```bash
# Add User-Agent to cURL requests
curl -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)" \
https://localhost/api/endpoint
# In API client code
$client = new GuzzleHttp\Client([
'headers' => [
'User-Agent' => 'MyApp/1.0'
]
]);
```
---
## Container/DI Issues
### Cannot Resolve Dependency
**Symptom**: `Cannot resolve parameter 'X' for class Y`
**Causes**:
- Constructor parameter has no type hint
- Scalar parameter without default value
- Interface not bound in container
**Solutions**:
```php
// ❌ Problem: No type hint
public function __construct($logger) {}
// ✅ Solution: Add type hint
public function __construct(private readonly Logger $logger) {}
// ❌ Problem: Scalar without default
public function __construct(string $apiKey) {}
// ✅ Solution: Use Value Object or default
public function __construct(
private readonly ApiKey $apiKey
) {}
// OR provide default
public function __construct(
private readonly string $apiKey = ''
) {}
```
**Interface Binding**:
```php
// Bind interface to implementation
#[Initializer]
public function initialize(Container $container): void
{
$container->bind(
LoggerInterface::class,
fn() => new FileLogger('/var/log/app.log')
);
}
```
---
### Circular Dependency Detected
**Symptom**: `Circular dependency detected: A -> B -> A`
**Solutions**:
```php
// ❌ Problem: Direct circular dependency
final readonly class OrderService
{
public function __construct(
private readonly InvoiceService $invoices
) {}
}
final readonly class InvoiceService
{
public function __construct(
private readonly OrderService $orders // Circular!
) {}
}
// ✅ Solution 1: Extract shared logic to third service
final readonly class OrderService
{
public function __construct(
private readonly PriceCalculator $calculator
) {}
}
final readonly class InvoiceService
{
public function __construct(
private readonly PriceCalculator $calculator
) {}
}
// ✅ Solution 2: Use events for decoupling
final readonly class OrderService
{
public function __construct(
private readonly EventDispatcher $events
) {}
public function completeOrder(Order $order): void
{
// Process order
$this->events->dispatch(new OrderCompletedEvent($order));
}
}
final readonly class InvoiceService
{
#[EventHandler]
public function onOrderCompleted(OrderCompletedEvent $event): void
{
// Generate invoice without direct dependency
$this->generateInvoice($event->order);
}
}
```
---
### Service Not Singleton
**Symptom**: Multiple instances created when singleton expected
**Solutions**:
```php
// ❌ Wrong: Regular binding creates new instance each time
$container->bind(
DatabaseConnection::class,
fn() => new DatabaseConnection($config)
);
// ✅ Correct: Singleton creates one instance
$container->singleton(
DatabaseConnection::class,
new DatabaseConnection($config)
);
// ✅ Alternative: Lazy singleton
$container->singleton(
DatabaseConnection::class,
fn() => new DatabaseConnection($config)
);
```
---
## Routing Problems
### Route Conflicts
**Symptom**: Wrong controller handling request
**Causes**:
- Multiple routes match same pattern
- Route priority issues
- Catch-all route defined too early
**Solutions**:
```bash
# List all routes to find conflicts
docker exec php php console.php routes:list | grep '/api/users'
# Check route priority order
docker exec php php console.php routes:list --sort=priority
```
**Fix Route Conflicts**:
```php
// ❌ Problem: Routes conflict
#[Route(path: '/api/users/{id}', method: Method::GET)]
public function getUser(string $id): JsonResult {}
#[Route(path: '/api/users/me', method: Method::GET)]
public function getCurrentUser(): JsonResult {} // Never matched!
// ✅ Solution: Most specific routes first
#[Route(path: '/api/users/me', method: Method::GET)]
public function getCurrentUser(): JsonResult {}
#[Route(path: '/api/users/{id}', method: Method::GET)]
public function getUser(string $id): JsonResult {}
```
---
### Route Parameters Not Working
**Symptom**: Route parameter is null or empty
**Solutions**:
```php
// ✅ Parameter name must match route
#[Route(path: '/api/users/{userId}', method: Method::GET)]
public function getUser(string $userId): JsonResult // Parameter name matches!
{
return new JsonResult(['userId' => $userId]);
}
// ❌ Wrong: Parameter name mismatch
#[Route(path: '/api/users/{userId}', method: Method::GET)]
public function getUser(string $id): JsonResult // Mismatch!
{
// $id will be null
}
```
---
### Middleware Not Executing
**Symptom**: Middleware bypassed or not running
**Causes**:
- Missing `#[MiddlewarePriority]` attribute
- Wrong middleware registration
- Middleware exception not caught
**Solutions**:
```php
// Add priority attribute
#[MiddlewarePriority(100)]
final readonly class AuthMiddleware implements Middleware
{
public function process(Request $request, callable $next): Response
{
// Auth logic
return $next($request);
}
}
// Check middleware is discovered
docker exec php php console.php debug:middleware
```
**Middleware Chain Debug**:
```php
// Add logging to trace execution
final readonly class DebugMiddleware implements Middleware
{
public function process(Request $request, callable $next): Response
{
Logger::debug('[Middleware] Before: ' . get_class($this));
$response = $next($request);
Logger::debug('[Middleware] After: ' . get_class($this));
return $response;
}
}
```
---
## Performance Issues
### Slow Page Load Times
**Diagnosis Steps**:
```bash
# 1. Enable performance profiling
docker exec php php console.php performance:enable
# 2. Check slow query log
docker exec php tail -f /var/log/mysql/slow-queries.log
# 3. Profile specific endpoint
docker exec php php console.php performance:profile /api/users
# 4. Check N+1 queries
docker exec php php console.php db:explain
```
**Common Causes & Fixes**:
#### N+1 Query Problem
```php
// ❌ Problem: N+1 queries
$users = $this->userRepository->findAll();
foreach ($users as $user) {
// Triggers separate query for each user!
$profile = $user->getProfile();
}
// ✅ Solution: Eager loading
$users = $this->userRepository->findAllWithProfiles();
// Single query with JOIN
```
#### Missing Indexes
```bash
# Check missing indexes
docker exec php php console.php db:analyze-indexes
# Add index via migration
$table->index('email'); // Single column
$table->index(['user_id', 'created_at']); // Composite
```
#### Cache Not Used
```php
// ✅ Cache expensive operations
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Duration;
$cacheKey = CacheKey::fromString('users_list');
$users = $this->cache->remember(
$cacheKey,
fn() => $this->repository->findAll(),
Duration::fromMinutes(10)
);
```
---
### Memory Exhaustion
**Symptom**: `Allowed memory size exhausted`
**Causes**:
- Large result sets loaded into memory
- Memory leaks in loops
- Circular references
**Solutions**:
```php
// ❌ Problem: Loading entire table
$allUsers = $this->connection->query('SELECT * FROM users');
// 1 million rows = memory exhausted
// ✅ Solution: Batch processing
$batchSize = 1000;
$offset = 0;
do {
$batch = $this->connection->query(
"SELECT * FROM users LIMIT {$batchSize} OFFSET {$offset}"
);
foreach ($batch as $user) {
$this->processUser($user);
}
$offset += $batchSize;
// Force garbage collection
gc_collect_cycles();
} while (count($batch) === $batchSize);
```
**Memory Leak Prevention**:
```php
// ✅ Clear references in loops
foreach ($largeDataset as $item) {
$result = $this->process($item);
// Clear large objects
unset($result, $item);
if ($iteration % 100 === 0) {
gc_collect_cycles();
}
}
```
---
## Database Connection Issues
### Connection Refused
**Symptom**: `SQLSTATE[HY000] [2002] Connection refused`
**Solutions**:
```bash
# 1. Check MySQL container is running
docker ps | grep mysql
# 2. Check database credentials in .env
DB_HOST=mysql # Use service name, not 'localhost'
DB_PORT=3306
DB_NAME=your_database
DB_USER=root
DB_PASS=your_password
# 3. Test connection directly
docker exec mysql mysql -u root -p your_database
# 4. Check MySQL logs
docker logs mysql
```
**Common Mistakes**:
```bash
# ❌ Wrong: localhost (from container perspective)
DB_HOST=localhost
# ✅ Correct: Docker service name
DB_HOST=mysql
```
---
### Too Many Connections
**Symptom**: `SQLSTATE[HY000] [1040] Too many connections`
**Causes**:
- Connection leaks (not closing connections)
- Too many concurrent requests
- Connection pool exhausted
**Solutions**:
```php
// ✅ Always use try-finally for connections
$connection = $this->connectionPool->getConnection();
try {
$connection->beginTransaction();
// Database operations
$connection->commit();
} catch (\Exception $e) {
$connection->rollback();
throw $e;
} finally {
// Always release connection
$this->connectionPool->releaseConnection($connection);
}
```
**Increase Connection Limit**:
```ini
# docker/mysql/my.cnf
[mysqld]
max_connections = 200
```
---
### Deadlock Detected
**Symptom**: `SQLSTATE[40001] Deadlock found when trying to get lock`
**Causes**:
- Transactions accessing tables in different order
- Long-running transactions
- Missing indexes causing lock escalation
**Solutions**:
```php
// ✅ Always access tables in consistent order
final readonly class PaymentService
{
public function transfer(Account $from, Account $to, Money $amount): void
{
$this->connection->beginTransaction();
try {
// ALWAYS lock in same order (e.g., by ID)
$accounts = [$from, $to];
usort($accounts, fn($a, $b) => $a->id <=> $b->id);
foreach ($accounts as $account) {
$this->lockAccount($account);
}
// Process transfer
$from->debit($amount);
$to->credit($amount);
$this->connection->commit();
} catch (\Exception $e) {
$this->connection->rollback();
throw $e;
}
}
}
```
**Retry Strategy**:
```php
// ✅ Retry on deadlock
public function executeWithRetry(callable $operation, int $maxAttempts = 3): mixed
{
$attempt = 0;
while ($attempt < $maxAttempts) {
try {
return $operation();
} catch (DeadlockException $e) {
$attempt++;
if ($attempt >= $maxAttempts) {
throw $e;
}
// Exponential backoff
usleep(pow(2, $attempt) * 100000); // 200ms, 400ms, 800ms
}
}
}
```
---
## Cache Problems
### Cache Not Working
**Symptom**: Data not cached, fetched every time
**Diagnosis**:
```bash
# 1. Check cache driver in .env
CACHE_DRIVER=redis # or file, array, memcached
# 2. Test cache directly
docker exec php php -r "
\$cache = new App\Framework\Cache\SmartCache();
\$cache->set('test', 'value', 60);
var_dump(\$cache->get('test'));
"
# 3. Check Redis connection (if using Redis)
docker exec redis redis-cli PING
# Should return: PONG
```
**Common Issues**:
```php
// ❌ Problem: TTL too short
$cache->set($key, $value, 1); // 1 second - expires immediately
// ✅ Solution: Appropriate TTL
use App\Framework\Core\ValueObjects\Duration;
$cache->set($key, $value, Duration::fromMinutes(10)->toSeconds());
// ✅ Better: Use CacheItem with Duration
$cacheItem = CacheItem::forSetting(
key: $key,
value: $value,
ttl: Duration::fromHours(1)
);
$cache->set($cacheItem);
```
---
### Cache Stampede
**Symptom**: Multiple processes regenerating same cached value simultaneously
**Solutions**:
```php
// ✅ Use cache locking
use App\Framework\Cache\CacheLock;
public function getExpensiveData(string $key): array
{
$cacheKey = CacheKey::fromString($key);
// Try to get from cache
$cached = $this->cache->get($cacheKey);
if ($cached !== null) {
return $cached;
}
// Acquire lock to prevent stampede
$lock = $this->cache->lock($cacheKey, Duration::fromSeconds(10));
if (!$lock->acquire()) {
// Another process is regenerating, wait for it
sleep(1);
return $this->cache->get($cacheKey) ?? [];
}
try {
// Regenerate data
$data = $this->expensiveOperation();
// Cache it
$this->cache->set(
CacheItem::forSetting($cacheKey, $data, Duration::fromMinutes(10))
);
return $data;
} finally {
$lock->release();
}
}
```
---
### Cache Invalidation Issues
**Symptom**: Stale data served from cache
**Solutions**:
```php
// ✅ Tag-based invalidation
use App\Framework\Cache\CacheTag;
// Cache with tags
$userTag = CacheTag::fromString("user_{$userId}");
$teamTag = CacheTag::fromString("team_{$teamId}");
$cacheItem = CacheItem::forSetting(
key: CacheKey::fromString("user_profile_{$userId}"),
value: $userProfile,
ttl: Duration::fromHours(1),
tags: [$userTag, $teamTag]
);
$this->cache->set($cacheItem);
// Invalidate all user-related caches
$this->cache->forget($userTag);
// Invalidate all team-related caches
$this->cache->forget($teamTag);
```
**Event-Based Invalidation**:
```php
// ✅ Automatic invalidation via events
final readonly class CacheInvalidationListener
{
#[EventHandler]
public function onUserUpdated(UserUpdatedEvent $event): void
{
$userTag = CacheTag::fromString("user_{$event->userId}");
$this->cache->forget($userTag);
}
}
```
---
## Debug Tools
### Framework Debug Mode
```bash
# Enable debug mode in .env
APP_ENV=development
APP_DEBUG=true
# Enhanced error pages with:
# - Full stack traces
# - Variable dumps
# - SQL query logs
# - Performance metrics
```
---
### Performance Profiling
```bash
# Enable performance collector
docker exec php php console.php performance:enable
# Profile specific request
docker exec php php console.php performance:profile /api/users
# View performance report
docker exec php php console.php performance:report
# Disable profiling
docker exec php php console.php performance:disable
```
---
### Database Query Logging
```php
// Enable query logging in code
use App\Framework\Database\QueryLogger;
$queryLogger = new QueryLogger();
$connection->setQueryLogger($queryLogger);
// Log queries
$users = $connection->query('SELECT * FROM users');
// View logged queries
foreach ($queryLogger->getQueries() as $query) {
Logger::debug('[SQL]', [
'query' => $query['sql'],
'bindings' => $query['bindings'],
'duration' => $query['duration']
]);
}
```
**Detect N+1 Queries**:
```bash
# Run N+1 detection
docker exec php php console.php db:detect-n-plus-one /api/users
```
---
### MCP Server Debugging
```bash
# Test MCP server connection
echo '{"jsonrpc": "2.0", "method": "initialize", "params": {}}' | \
docker exec -i php php console.php mcp:server
# Analyze routes via MCP
docker exec php php console.php mcp:analyze routes
# Analyze container bindings
docker exec php php console.php mcp:analyze container
# Check framework health
docker exec php php console.php mcp:health
```
---
### Request Debugging
```php
// Log full request details
use App\Framework\Http\Request;
Logger::debug('[Request]', [
'method' => $request->method->value,
'path' => $request->path,
'query' => $request->queryParameters ?? [],
'body' => $request->parsedBody ?? [],
'headers' => $request->headers->toArray(),
'server' => [
'ip' => $request->server->getRemoteAddr(),
'user_agent' => $request->server->getUserAgent()
]
]);
```
---
### Event System Debugging
```php
// Log all dispatched events
final readonly class EventDebugListener
{
#[EventHandler]
public function onAnyEvent(object $event): void
{
Logger::debug('[Event]', [
'type' => get_class($event),
'data' => $event
]);
}
}
```
---
## Emergency Procedures
### Application Down
**Quick Recovery Steps**:
```bash
# 1. Check Docker containers
docker ps -a
# 2. Restart all services
make down && make up
# 3. Check logs
make logs
# 4. Verify database connection
docker exec mysql mysql -u root -p
# 5. Clear all caches
rm -rf storage/cache/*
composer reload
# 6. Test health endpoint
curl -k https://localhost/health
```
---
### Database Corruption
```bash
# 1. Stop application
make down
# 2. Backup database immediately
docker exec mysql mysqldump -u root -p your_database > backup.sql
# 3. Check and repair tables
docker exec mysql mysqlcheck -u root -p --auto-repair your_database
# 4. Restore from backup if needed
docker exec -i mysql mysql -u root -p your_database < backup.sql
# 5. Restart application
make up
```
---
### Cache Corruption
```bash
# 1. Flush all caches
docker exec redis redis-cli FLUSHALL # If using Redis
rm -rf storage/cache/* # File cache
# 2. Restart services
make restart
# 3. Warm up critical caches
docker exec php php console.php cache:warm
```
---
## Getting Help
### Framework Support Channels
**Issues & Bug Reports**:
- GitHub Issues: Report bugs with reproduction steps
- Include: PHP version, Docker logs, error messages
**Documentation**:
- `/docs/claude/` - Complete framework documentation
- CLAUDE.md - AI assistant integration guide
**Debugging Resources**:
- MCP Server - AI-powered framework analysis
- Performance Profiler - Built-in performance monitoring
- Query Logger - SQL debugging tool
---
## Prevention Best Practices
### Development Checklist
- [ ] Run `composer reload` after creating new classes
- [ ] Clear caches after config changes
- [ ] Test with HTTPS in development
- [ ] Add User-Agent to API requests
- [ ] Use Value Objects instead of primitives
- [ ] Implement proper error handling
- [ ] Add logging to critical operations
- [ ] Test with production-like data volumes
- [ ] Profile performance before deployment
- [ ] Review query execution plans
### Code Review Checklist
- [ ] No circular dependencies
- [ ] Proper exception handling
- [ ] Database transactions properly closed
- [ ] Cache keys properly namespaced
- [ ] Routes don't conflict
- [ ] Middleware properly ordered
- [ ] Security best practices followed
- [ ] Performance considerations addressed
- [ ] Tests cover critical paths
- [ ] Documentation updated
---
## Appendix: Common Error Codes
| Error Code | Meaning | Common Cause |
|------------|---------|--------------|
| 403 | Forbidden | WAF blocking, missing Auth, IP restriction |
| 404 | Not Found | Route not registered, wrong path |
| 405 | Method Not Allowed | HTTP method mismatch in Route attribute |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unhandled exception, see logs |
| 502 | Bad Gateway | PHP-FPM down, connection refused |
| 503 | Service Unavailable | Database down, cache unavailable |
---
**Last Updated**: 2025-01-28
**Framework Version**: 2.x