getgrav/grav · error · RuntimeException

ZipArchiver: Failed to open {archive_file}

Error message

ZipArchiver: Failed to open {archive_file}

What it means

ZipArchiver::extract() begins with $zip->open($this->archive_file); any result other than true (corrupt archive, missing file, unreadable permissions, wrong format) skips the whole extraction body and falls through to RuntimeException 'ZipArchiver: Failed to open <file>'. Unlike compress(), this path does not map the ZipArchive error code to a reason, so the caller only learns the open failed. It is the entry-point integrity gate for every extraction.

Source

Thrown at system/src/Grav/Common/Filesystem/ZipArchiver.php:112

            Folder::create($destination);

            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).
                $this->extractStreamed($zip, $destination, $numFiles, $maxSize);
            } elseif (!$zip->extractTo($destination)) {
                $zip->close();
                throw new RuntimeException('ZipArchiver: ZIP failed to extract ' . $this->archive_file . ' to ' . $destination);
            }

            $zip->close();

            return $this;
        }

        throw new RuntimeException('ZipArchiver: Failed to open ' . $this->archive_file);
    }

    /**
     * Resolve the uncompressed-size / file-count / nesting-depth caps applied
     * before extraction. Shares the same config keys and defaults as
     * GPM\Installer so both ZIP extraction paths enforce identical limits. A
     * limit of 0 disables that particular check.
     *
     * @return array{0:int,1:int,2:int} [maxUncompressedBytes, maxFiles, maxDepth]
     */
    protected function archiveLimits(): array
    {
        $config = Grav::instance()['config'] ?? null;

        $maxSize  = $config ? (int) $config->get('system.gpm.archive.max_uncompressed_size', 1073741824) : 1073741824; // 1 GiB
        $maxFiles = $config ? (int) $config->get('system.gpm.archive.max_files', 50000) : 50000;
        $maxDepth = $config ? (int) $config->get('system.gpm.archive.max_depth', 48) : 48;

View on GitHub (pinned to 6040efed04)

Solutions

  1. Confirm the file exists, is readable (is_file/is_readable), and has a plausible size — a few-KB 'zip' is often an HTML error page
  2. Validate integrity outside PHP: unzip -t file.zip (exit code 0 = intact); re-download the package from its official source if broken
  3. Check the ZipArchive open error yourself first (see validationCode) to get the specific ER_* code instead of the generic message
  4. Ensure the zip PHP extension and libzip are current if large/zip64 archives are involved

Example fix

// before
(new ZipArchiver($zipPath))->extract($destination); // generic 'Failed to open'

// after — surface the real ZipArchive error first
$zip = new ZipArchive();
$res = $zip->open($zipPath);
if ($res !== true) {
    throw new RuntimeException(sprintf('Cannot open %s: ZipArchive error code %d', $zipPath, $res));
}
$zip->close();
(new ZipArchiver($zipPath))->extract($destination);
Defensive patterns

Strategy: validation

Validate before calling

if (!is_file($zipPath) || !is_readable($zipPath) || filesize($zipPath) < 22) {
    throw new RuntimeException("{$zipPath} is not a readable zip file");
}
$probe = new ZipArchive();
$res = $probe->open($zipPath);
if ($res !== true) {
    throw new RuntimeException(sprintf('ZipArchive refuses %s: error code %d', $zipPath, $res));
}
$probe->close();

Try / catch

try {
    (new ZipArchiver($path))->extract($destination);
} catch (RuntimeException $e) {
    if (str_contains($e->getMessage(), 'Failed to open')) {
        // corrupt/truncated/unreadable archive — re-download from a trusted source, then retry
    }
}

Prevention

When it happens

Trigger: Extracting a file that is not a real zip (HTML error page saved as .zip from a failed download); truncated/interrupted download; file removed or unreadable between setArchive() and extract(); encrypted archives opened without a password; zip extension reading a >4 GiB zip64 file on an old libzip.

Common situations: GPM package downloads interrupted by proxies returning partial content; manually uploaded packages with wrong extension; permissions changed after upload; exotic zip variants produced by nonstandard packers.

Related errors


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