BookStackApp/BookStack · error · ThemeModuleException

Failed to load extract files from module ZIP with error: {$e

Error message

Failed to load extract files from module ZIP with error: {$exception->getMessage()}

What it means

ThemeModuleManager::addFromZip wraps failures from $zip->extractTo(): if extraction of a module ZIP throws, the partially extracted folder is removed and a new ThemeModuleException is thrown embedding the original error message. It indicates the ZIP contents could not be written to disk under the themes modules folder.

Source

Thrown at app/Theming/ThemeModuleManager.php:60

    /**
     * @throws ThemeModuleException
     */
    public function addFromZip(string $name, ThemeModuleZip $zip): ThemeModule
    {
        $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 {

View on GitHub (pinned to 18f8469a1c)

Solutions

  1. Check storage permissions on the themes modules folder (web-server user needs write access) and retry the upload
  2. Re-download or rebuild the ZIP and verify it opens with unzip -t
  3. Free disk space if the volume is full
  4. Read the embedded {$exception->getMessage()} in the message — it names the underlying cause (e.g. specific entry or path)
  5. Confirm the PHP zip extension is installed and current

Example fix

// before (debugging)
// message: Failed to load extract files from module ZIP with error: ...
// after (server check)
sudo -u www-data test -w themes/modules && echo writable
unzip -t my-module.zip
Defensive patterns

Strategy: try-catch

Validate before calling

$zipPath = $uploadedFile->getRealPath();
$zipArchive = new ZipArchive();
if ($zipArchive->open($zipPath) !== true || $zipArchive->numFiles === 0) {
    throw new InvalidArgumentException('ZIP is corrupt or empty');
}
if (disk_free_space(dirname($modulesFolderPath)) < 10 * 1024 * 1024) {
    throw new RuntimeException('Insufficient disk space');
}
if (!is_writable($modulesFolderPath)) {
    throw new RuntimeException('Modules folder not writable');
}

Type guard

function isUsableZip(string $path): bool {
    $z = new ZipArchive();
    return $z->open($path) === true && $z->numFiles > 0;
}

Try / catch

try {
    $module = $manager->addFromZip($name, $zip);
} catch (ThemeModuleException $e) {
    Log::error('Module ZIP extraction failed', ['reason' => $e->getMessage()]);
    // surface $e->getMessage() to the uploader; cleanup already handled by manager
}

Prevention

When it happens

Trigger: Installing a module via ThemeModuleZip::extractTo where the zip is corrupt/encrypted, an entry name is unsafe, the target path isn't writable, or disk space is exhausted. Raised through the 'handle' upload flow.

Common situations: Truncated or re-saved ZIP downloads; read-only storage directory after deployment/permission changes; full disk; malicious or malformed zip entries rejected by the extractor; PHP zip extension limitations.

Related errors


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