Skip to content

Bug Report: Cache Clear Operation Fails with HTTP 500 #4

@xinpehr

Description

@xinpehr

Summary

Clicking the "Clear Cache" button in the admin panel returns a 500 Internal Server Error,
preventing cache from being cleared.

Environment

  • PHP with Symfony Cache (symfony/cache ^7.2)
  • Application using set_error_handler() to convert PHP warnings to ErrorException

Root Cause

In Application::warningHandler(), all PHP E_WARNING errors are converted to ErrorException:

set_error_handler($this->warningHandler(...), E_WARNING);

private function warningHandler(int $errno, string $errstr, ...): never {
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}

CacheManager::clearCacheDir() calls rmdir() and unlink() without error suppression.
When the Symfony FilesystemAdapter clears the cache pool (cacheItemPool->clear()), it
deletes cache files but not all subdirectories. When clearCacheDir() subsequently tries
to rmdir() a directory that is still non-empty (due to concurrent writes from other
PHP processes writing new cache entries during the clear operation), rmdir() emits a
PHP warning, which warningHandler() immediately converts to an ErrorException.
This exception propagates up through ClearCacheRequestHandler and is caught by
ExceptionMiddleware, which returns a 500 response.

Log evidence of the failure:
ErrorException: rmdir(/home/.../var/cache/@): Directory not empty
#0 [internal function]: Application->warningHandler()
#1 CacheManager.php(44): rmdir()
#2 CacheManager.php(28): CacheManager->clearCacheDir()
#3 ClearCacheRequestHandler.php(26): CacheManager->clearCache()

Affected File

src/Shared/Infrastructure/CacheManager.php

private function clearCacheDir(): void
{
    foreach ($iterator as $path) {
        if ($path->isDir()) {
            rmdir($path->getPathname());      // <-- triggers warning if not empty
        } elseif ($path->getFilename() != '.gitkeep') {
            unlink($path->getPathname());     // <-- triggers warning if already deleted
        }
    }
}

Proposed Fix

Suppress warnings on filesystem operations, exactly as Symfony's own FilesystemAdapter
does internally in FilesystemCommonTrait::doUnlink() with @Unlink():

private function clearCacheDir(): void
{
    foreach ($iterator as $path) {
        if ($path->isDir()) {
            @rmdir($path->getPathname());
        } elseif ($path->getFilename() != '.gitkeep') {
            @unlink($path->getPathname());
        }
    }
}

The @ operator suppresses the PHP warning before warningHandler() can intercept it,
allowing the loop to continue gracefully when a directory is non-empty or a file was
already deleted by a concurrent process.

Steps to Reproduce

  1. Have any module writing to the Symfony FilesystemAdapter cache (e.g. embedding cache,
    IP geolocation cache, or any PSR-16 cache usage) with ongoing traffic.
  2. Navigate to Admin → Settings.
  3. Click "Clear Cache".
  4. Observe HTTP 500 response.

Expected Behavior

Cache is cleared successfully and the UI shows a success notification.

Actual Behavior

HTTP 500 is returned. No cache is cleared.

Notes

  • The bug is latent in the original code and becomes reliably triggered once any
    service actively writes to the cache with sufficient concurrency.
  • No changes to Application::warningHandler() are required; the fix is isolated
    to CacheManager::clearCacheDir().

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions