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

22 KiB

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:

# 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:

// 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:

# 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:

// ❌ 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:

# 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: 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:

# 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:

// ❌ 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:

// 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:

// ❌ 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:

// ❌ 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:

# 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:

// ❌ 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:

// ✅ 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:

// 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:

// 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:

# 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

// ❌ 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

# 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

// ✅ 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:

// ❌ 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:

// ✅ 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:

# 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:

# ❌ 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:

// ✅ 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:

# 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:

// ✅ 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:

// ✅ 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:

# 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:

// ❌ 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:

// ✅ 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:

// ✅ 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:

// ✅ 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

# 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

# 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

// 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:

# Run N+1 detection
docker exec php php console.php db:detect-n-plus-one /api/users

MCP Server Debugging

# 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

// 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

// 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:

# 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

# 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

# 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