{"record":{"id":"1169bc6cc6fee539","repo":"getgrav/grav","slug":"bad-package-file-s","errorCode":null,"errorMessage":"Bad package file: %s","messagePattern":"Bad package file: (.+?)","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"system/src/Grav/Common/GPM/Installer.php","lineNumber":268,"sourceCode":"            if ($maxSize > 0) {\n                // Enforce the uncompressed-size cap against bytes actually written,\n                // so a forged-small declared size cannot smuggle a bomb past the\n                // advisory pre-pass above (GHSA-8h9x-89f2-m7x3). On failure the\n                // helper sets self::$error and removes the destination.\n                if (!self::extractStreamed($zip, $destination, $numFiles, $maxSize)) {\n                    $zip->close();\n                    return false;\n                }\n            } elseif (!$zip->extractTo($destination)) {\n                self::$error = self::ZIP_EXTRACT_ERROR;\n                Folder::delete($destination);\n                $zip->close();\n                return false;\n            }\n\n            $package_folder_name = $zip->getNameIndex(0);\n            if ($package_folder_name === false) {\n                throw new \\RuntimeException('Bad package file: ' . Utils::basename($zip_file));\n            }\n            $package_folder_name = preg_replace('#\\./$#', '', $package_folder_name);\n            $zip->close();\n\n            self::$error = self::OK;\n\n            return $destination . '/' . $package_folder_name;\n        }\n\n        self::$error = self::ZIP_EXTRACT_ERROR;\n        self::$error_zip = $archive;\n\n        return false;\n    }\n\n    /**\n     * Reject Zip Slip primitives in archive entry names: empty names, NUL\n     * bytes, absolute paths, or any path segment that is `..`. Forward and","sourceCodeStart":250,"sourceCodeEnd":286,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/Grav/Common/GPM/Installer.php#L250-L286","documentation":"Installer::install() extracts the package zip, then reads the first entry name via $zip->getNameIndex(0) (line 268) to learn the package's root folder. When that returns false the archive contains no entries at all — an empty or truncated zip — so Grav throws rather than guess a destination path. Everything before this point (open, extract) succeeded, which is exactly the signature of an empty archive.","triggerScenarios":"Installing a package whose zip has zero central-directory entries: an empty file renamed to .zip, a partially downloaded/corrupted archive, or a zip produced by a failed packaging step; direct Installer::install() calls with such a file.","commonSituations":"Install-from-URL where the server returned an HTML error page or empty 200 response saved as a .zip; interrupted downloads on slow connections; CI-built plugin/theme zips whose packaging command failed silently.","solutions":["Re-download the package from the source and retry — a truncated download is the most common cause.","Pre-validate the archive before installing: open it with ZipArchive and require numFiles >= 1 so you can fail with a clearer message than the installer's.","If you build the package yourself, ensure the zip actually contains the expected top-level folder (e.g. 'my-plugin/...')."],"exampleFix":"// before\n$result = Installer::install($zipPath, $installPath);\n\n// after\n$zip = new \\ZipArchive();\nif (true !== $zip->open($zipPath) || $zip->numFiles < 1) {\n    throw new \\RuntimeException('The downloaded package is empty or not a valid zip archive.');\n}\n$zip->close();\n$result = Installer::install($zipPath, $installPath);","handlingStrategy":"validation","validationCode":"$zip = new \\ZipArchive();\n$ok = true === $zip->open($zipPath) && $zip->numFiles >= 1;\nif ($ok) { $zip->close(); }\nif (!$ok) {\n    throw new \\RuntimeException('Package archive is empty or corrupt — re-download it.');\n}\nInstaller::install($zipPath, $installPath);","typeGuard":"function isNonEmptyZip(string $path): bool\n{\n    $z = new \\ZipArchive();\n    $ok = true === $z->open($path) && $z->numFiles >= 1;\n    if ($ok) { $z->close(); }\n    return $ok;\n}","tryCatchPattern":"try {\n    Installer::install($zipPath, $installPath);\n} catch (\\RuntimeException $e) {\n    if (str_starts_with($e->getMessage(), 'Bad package file')) {\n        // delete the downloaded file, re-fetch once, then retry install\n    }\n}","preventionTips":["Verify Content-Length and content-type when saving downloaded packages.","Check zip entry counts before handing archives to Installer.","Keep packaging steps in CI fail-loud so empty zips never ship."],"tags":["gpm","installer","zip","package-management","download"],"backgroundTag":"corrupt-archive-download","analyzedSha":"6040efed04efa69b8209448ed81308e7c24147c2","analyzedAt":"2026-08-17T05:07:31.593Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}