getgrav/grav · error · RuntimeException

Bad package file: %s

Error message

Bad package file: %s

What it means

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.

Source

Thrown at system/src/Grav/Common/GPM/Installer.php:268

            if ($maxSize > 0) {
                // Enforce the uncompressed-size cap against bytes actually written,
                // so a forged-small declared size cannot smuggle a bomb past the
                // advisory pre-pass above (GHSA-8h9x-89f2-m7x3). On failure the
                // helper sets self::$error and removes the destination.
                if (!self::extractStreamed($zip, $destination, $numFiles, $maxSize)) {
                    $zip->close();
                    return false;
                }
            } elseif (!$zip->extractTo($destination)) {
                self::$error = self::ZIP_EXTRACT_ERROR;
                Folder::delete($destination);
                $zip->close();
                return false;
            }

            $package_folder_name = $zip->getNameIndex(0);
            if ($package_folder_name === false) {
                throw new \RuntimeException('Bad package file: ' . Utils::basename($zip_file));
            }
            $package_folder_name = preg_replace('#\./$#', '', $package_folder_name);
            $zip->close();

            self::$error = self::OK;

            return $destination . '/' . $package_folder_name;
        }

        self::$error = self::ZIP_EXTRACT_ERROR;
        self::$error_zip = $archive;

        return false;
    }

    /**
     * Reject Zip Slip primitives in archive entry names: empty names, NUL
     * bytes, absolute paths, or any path segment that is `..`. Forward and

View on GitHub (pinned to 6040efed04)

Solutions

  1. Re-download the package from the source and retry — a truncated download is the most common cause.
  2. 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.
  3. If you build the package yourself, ensure the zip actually contains the expected top-level folder (e.g. 'my-plugin/...').

Example fix

// before
$result = Installer::install($zipPath, $installPath);

// after
$zip = new \ZipArchive();
if (true !== $zip->open($zipPath) || $zip->numFiles < 1) {
    throw new \RuntimeException('The downloaded package is empty or not a valid zip archive.');
}
$zip->close();
$result = Installer::install($zipPath, $installPath);
Defensive patterns

Strategy: validation

Validate before calling

$zip = new \ZipArchive();
$ok = true === $zip->open($zipPath) && $zip->numFiles >= 1;
if ($ok) { $zip->close(); }
if (!$ok) {
    throw new \RuntimeException('Package archive is empty or corrupt — re-download it.');
}
Installer::install($zipPath, $installPath);

Type guard

function isNonEmptyZip(string $path): bool
{
    $z = new \ZipArchive();
    $ok = true === $z->open($path) && $z->numFiles >= 1;
    if ($ok) { $z->close(); }
    return $ok;
}

Try / catch

try {
    Installer::install($zipPath, $installPath);
} catch (\RuntimeException $e) {
    if (str_starts_with($e->getMessage(), 'Bad package file')) {
        // delete the downloaded file, re-fetch once, then retry install
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/1169bc6cc6fee539. Report an issue: GitHub.