{"record":{"id":"7acb5600e1f82485","repo":"BookStackApp/BookStack","slug":"failed-to-load-module-from-zip-file-after-extracti","errorCode":null,"errorMessage":"Failed to load module from zip file after extraction","messagePattern":"Failed to load module from zip file after extraction","errorType":"exception","errorClass":"ThemeModuleException","httpStatus":null,"severity":"error","filePath":"app/Theming/ThemeModuleManager.php","lineNumber":65,"sourceCode":"        $baseFolderName = Str::limit(Str::slug($name), 40, '');\n        $folderName = $baseFolderName;\n        while (!$baseFolderName || file_exists($this->modulesFolderPath . DIRECTORY_SEPARATOR . $folderName)) {\n            $folderName = ($baseFolderName ?: 'mod') . '-' . Str::random(4);\n        }\n\n        $folderPath = $this->modulesFolderPath . DIRECTORY_SEPARATOR . $folderName;\n        try {\n            $zip->extractTo($folderPath);\n        } catch (ThemeModuleException $exception) {\n            if (is_dir($folderPath)) {\n                $this->deleteDirectoryRecursively($folderPath);\n            }\n            throw new ThemeModuleException(\"Failed to load extract files from module ZIP with error: {$exception->getMessage()}\");\n        }\n\n        $module = $this->loadFromFolder($folderName);\n        if (!$module) {\n            throw new ThemeModuleException(\"Failed to load module from zip file after extraction\");\n        }\n\n        return $module;\n    }\n\n    protected function deleteDirectoryRecursively(string $path): void\n    {\n        $items = array_diff(scandir($path), ['.', '..']);\n        foreach ($items as $item) {\n            $itemPath = $path . DIRECTORY_SEPARATOR . $item;\n            if (is_dir($itemPath)) {\n                $this->deleteDirectoryRecursively($itemPath);\n            } else {\n                $deleted = unlink($itemPath);\n                if (!$deleted) {\n                    throw new ThemeModuleException(\"Failed to delete file at \\\"{$itemPath}\\\"\");\n                }\n            }","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/BookStackApp/BookStack/blob/18f8469a1c72f8cc8497e9372635e6dea5028071/app/Theming/ThemeModuleManager.php#L47-L83","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","solutions":["Repackage the ZIP so the module descriptor sits at the depth BookStack's loader expects (no single extra wrapper folder), then re-upload","Ensure the ZIP actually contains a valid module descriptor (valid name/description/version per ThemeModule::fromJson)","Download a release ZIP rather than an auto-generated source archive with a renamed root folder","Check server logs / try unzipping manually to inspect the actual folder layout"],"exampleFix":"// before: zip layout\nmy-module-main/module.php\n// after: zip layout (files at expected root)\nmodule.php\n// or: my-module/module.php packaged as my-module.zip","handlingStrategy":"validation","validationCode":"$zip = new ZipArchive();\n$zip->open($zipPath);\n$hasDescriptor = false;\nfor ($i = 0; $i < $zip->numFiles; $i++) {\n    $name = $zip->getNameIndex($i);\n    if (preg_match('#^[^/]+/(module\\.php|[^/]*\\.json)$#', $name) || preg_match('#^[^/]+$#', $name)) {\n        $hasDescriptor = true; // descriptor at expected depth\n    }\n}\nif (!$hasDescriptor) {\n    throw new InvalidArgumentException('ZIP must contain a module descriptor at the expected location (no extra wrapper folder)');\n}","typeGuard":"function zipLooksLikeModule(string $zipPath): bool {\n    $z = new ZipArchive();\n    if ($z->open($zipPath) !== true) return false;\n    for ($i = 0; $i < $z->numFiles; $i++) {\n        if (str_contains($z->getNameIndex($i), 'module.php')) return true;\n    }\n    return false;\n}","tryCatchPattern":"try {\n    $module = $manager->addFromZip($name, $zip);\n} catch (ThemeModuleException $e) {\n    if ($e->getMessage() === 'Failed to load module from zip file after extraction') {\n        // inspect extracted layout; likely missing descriptor or extra nesting\n    }\n}","preventionTips":["Package ZIPs so the descriptor file is at the root or the single expected folder level","Avoid GitHub 'Download ZIP' source archives with auto-renamed wrapper folders — repackage instead","Include a valid module descriptor (name/description/version) in every release ZIP","Manually extract and inspect the ZIP before uploading if unsure"],"tags":["zip","theming","validation","packaging","bookstack"],"backgroundTag":"module-descriptor-not-found","analyzedSha":"18f8469a1c72f8cc8497e9372635e6dea5028071","analyzedAt":"2026-09-02T19:49:33.068Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}