flarum/framework · critical · UnreadableManifestException
Cannot read the installed package manifest at $path…
Error message
Cannot read the installed package manifest at $path: $reason. Flarum cannot determine which extensions are installed until this is resolved, which usually means completing or re-running `composer install`. (the file is not valid JSON, so it may be corrupt or only partially written)
What it means
After loading composer's installed.json, ExtensionManager checks json_decode result; a non-array means the manifest is not valid JSON, so UnreadableManifestException::unparsable() is thrown — the file exists but is corrupt, empty, or only partially written.
Solutions
- Re-run `composer install` to regenerate a valid installed.json
- Delete the corrupt vendor/composer/installed.json and reinstall
- Ensure only one composer process runs at a time during deploys
- Validate the file: `php -r 'var_dump(is_array(json_decode(file_get_contents("vendor/composer/installed.json"), true)));'`
Example fix
// before
# installed.json truncated: '{"packages": ['
// after
composer install # rewrites a valid installed.json Defensive patterns
Strategy: try-catch
Validate before calling
$data = json_decode(file_get_contents($vendor.'/composer/installed.json'), true);
if (!is_array($data)) { /* regenerate before boot */ } Type guard
function validManifest(string $vendor): bool {
$raw = @file_get_contents($vendor.'/composer/installed.json');
return is_array(json_decode($raw ?? '', true));
} Try / catch
try {
$extensions = $manager->getExtensions();
} catch (UnreadableManifestException $e) {
logger()->critical('installed.json corrupt: rerun composer install');
abort(500);
} Prevention
- Never run two composer processes against the same app concurrently
- Use atomic deploys (build vendor in a temp dir, then symlink/swap)
- Checksum installed.json in deploy verification
- Keep composer install inside its own completed deploy step
When it happens
Trigger: getExtensions() reads {vendor}/composer/installed.json and json_decode returns null/scalar instead of an array (truncated write, concurrent composer run, disk-full, manual edits).
Common situations: Deploys interrupted mid-composer, two composer processes racing on the same install, corrupted artifacts, or files damaged by bad rsync/checkout.
Related errors
AI-assisted analysis of flarum/framework@4b939f6853 (2026-09-15).
Data as JSON: /api/errors/089843902971e1ba.
Report an issue: GitHub.
Appendix: source
Thrown at framework/core/src/Extension/ExtensionManager.php:69
* @return Collection<string, Extension>
*/
public function getExtensions(): Collection
{
if (is_null($this->extensions)) {
$manifest = $this->paths->vendor.'/composer/installed.json';
if (! $this->filesystem->exists($manifest)) {
throw UnreadableManifestException::missing($manifest);
}
/** @var Collection<string, Extension> $extensions */
$extensions = new Collection();
// Load all packages installed by composer.
$installed = json_decode($this->filesystem->get($manifest), true);
if (! is_array($installed)) {
throw UnreadableManifestException::unparsable($manifest);
}
// Composer 2.0 changes the structure of the installed.json manifest
$installed = $installed['packages'] ?? $installed;
if (! is_array($installed)) {
throw UnreadableManifestException::unparsable($manifest);
}
// We calculate and store a set of composer package names for all installed Flarum extensions,
// so we know what is and isn't a flarum extension in `calculateDependencies`.
// Using keys of an associative array allows us to do these checks in constant time.
$installedSet = [];
$composerJsonConfs = [];
foreach ($installed as $package) {
$name = Arr::get($package, 'name');View on GitHub (pinned to 4b939f6853)