BookStackApp/BookStack · error · ThemeModuleException

Failed to load module from zip file after extraction

Error message

Failed to load module from zip file after extraction

What it means

After a module ZIP extracts successfully, addFromZip calls loadFromFolder to parse it; if that returns null (no valid ThemeModule found in the extracted folder), this ThemeModuleException is thrown. The ZIP contained files, but not a recognizable module structure/descriptor at the expected location.

Source

Thrown at app/Theming/ThemeModuleManager.php:65

        $baseFolderName = Str::limit(Str::slug($name), 40, '');
        $folderName = $baseFolderName;
        while (!$baseFolderName || file_exists($this->modulesFolderPath . DIRECTORY_SEPARATOR . $folderName)) {
            $folderName = ($baseFolderName ?: 'mod') . '-' . Str::random(4);
        }

        $folderPath = $this->modulesFolderPath . DIRECTORY_SEPARATOR . $folderName;
        try {
            $zip->extractTo($folderPath);
        } catch (ThemeModuleException $exception) {
            if (is_dir($folderPath)) {
                $this->deleteDirectoryRecursively($folderPath);
            }
            throw new ThemeModuleException("Failed to load extract files from module ZIP with error: {$exception->getMessage()}");
        }

        $module = $this->loadFromFolder($folderName);
        if (!$module) {
            throw new ThemeModuleException("Failed to load module from zip file after extraction");
        }

        return $module;
    }

    protected function deleteDirectoryRecursively(string $path): void
    {
        $items = array_diff(scandir($path), ['.', '..']);
        foreach ($items as $item) {
            $itemPath = $path . DIRECTORY_SEPARATOR . $item;
            if (is_dir($itemPath)) {
                $this->deleteDirectoryRecursively($itemPath);
            } else {
                $deleted = unlink($itemPath);
                if (!$deleted) {
                    throw new ThemeModuleException("Failed to delete file at \"{$itemPath}\"");
                }
            }

View on GitHub (pinned to 18f8469a1c)

Solutions

  1. Repackage the ZIP so the module descriptor sits at the depth BookStack's loader expects (no single extra wrapper folder), then re-upload
  2. Ensure the ZIP actually contains a valid module descriptor (valid name/description/version per ThemeModule::fromJson)
  3. Download a release ZIP rather than an auto-generated source archive with a renamed root folder
  4. Check server logs / try unzipping manually to inspect the actual folder layout

Example fix

// before: zip layout
my-module-main/module.php
// after: zip layout (files at expected root)
module.php
// or: my-module/module.php packaged as my-module.zip
Defensive patterns

Strategy: validation

Validate before calling

$zip = new ZipArchive();
$zip->open($zipPath);
$hasDescriptor = false;
for ($i = 0; $i < $zip->numFiles; $i++) {
    $name = $zip->getNameIndex($i);
    if (preg_match('#^[^/]+/(module\.php|[^/]*\.json)$#', $name) || preg_match('#^[^/]+$#', $name)) {
        $hasDescriptor = true; // descriptor at expected depth
    }
}
if (!$hasDescriptor) {
    throw new InvalidArgumentException('ZIP must contain a module descriptor at the expected location (no extra wrapper folder)');
}

Type guard

function zipLooksLikeModule(string $zipPath): bool {
    $z = new ZipArchive();
    if ($z->open($zipPath) !== true) return false;
    for ($i = 0; $i < $z->numFiles; $i++) {
        if (str_contains($z->getNameIndex($i), 'module.php')) return true;
    }
    return false;
}

Try / catch

try {
    $module = $manager->addFromZip($name, $zip);
} catch (ThemeModuleException $e) {
    if ($e->getMessage() === 'Failed to load module from zip file after extraction') {
        // inspect extracted layout; likely missing descriptor or extra nesting
    }
}

Prevention

When it happens

Trigger: Uploading a ZIP whose module descriptor is missing, misnamed, or nested one or more directories deeper than the loader expects, so loadFromFolder finds no valid module.

Common situations: ZIP wrapping everything in an extra top-level folder (e.g. my-module-main/ from a GitHub download); uploading a source repo ZIP without a descriptor file; uploading the wrong ZIP entirely (assets-only build).

Related errors


AI-assisted analysis of BookStackApp/BookStack@18f8469a1c (2026-09-02). Data as JSON: /api/errors/7acb5600e1f82485. Report an issue: GitHub.