{"record":{"id":"7d41d6748b29466e","repo":"getgrav/grav","slug":"ziparchiver-failed-to-open-archive-file","errorCode":null,"errorMessage":"ZipArchiver: Failed to open {archive_file}","messagePattern":"ZipArchiver: Failed to open (.+?)","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"system/src/Grav/Common/Filesystem/ZipArchiver.php","lineNumber":112,"sourceCode":"\n            Folder::create($destination);\n\n            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).\n                $this->extractStreamed($zip, $destination, $numFiles, $maxSize);\n            } elseif (!$zip->extractTo($destination)) {\n                $zip->close();\n                throw new RuntimeException('ZipArchiver: ZIP failed to extract ' . $this->archive_file . ' to ' . $destination);\n            }\n\n            $zip->close();\n\n            return $this;\n        }\n\n        throw new RuntimeException('ZipArchiver: Failed to open ' . $this->archive_file);\n    }\n\n    /**\n     * Resolve the uncompressed-size / file-count / nesting-depth caps applied\n     * before extraction. Shares the same config keys and defaults as\n     * GPM\\Installer so both ZIP extraction paths enforce identical limits. A\n     * limit of 0 disables that particular check.\n     *\n     * @return array{0:int,1:int,2:int} [maxUncompressedBytes, maxFiles, maxDepth]\n     */\n    protected function archiveLimits(): array\n    {\n        $config = Grav::instance()['config'] ?? null;\n\n        $maxSize  = $config ? (int) $config->get('system.gpm.archive.max_uncompressed_size', 1073741824) : 1073741824; // 1 GiB\n        $maxFiles = $config ? (int) $config->get('system.gpm.archive.max_files', 50000) : 50000;\n        $maxDepth = $config ? (int) $config->get('system.gpm.archive.max_depth', 48) : 48;\n","sourceCodeStart":94,"sourceCodeEnd":130,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/Grav/Common/Filesystem/ZipArchiver.php#L94-L130","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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","Validate integrity outside PHP: unzip -t file.zip (exit code 0 = intact); re-download the package from its official source if broken","Check the ZipArchive open error yourself first (see validationCode) to get the specific ER_* code instead of the generic message","Ensure the zip PHP extension and libzip are current if large/zip64 archives are involved"],"exampleFix":"// before\n(new ZipArchiver($zipPath))->extract($destination); // generic 'Failed to open'\n\n// after — surface the real ZipArchive error first\n$zip = new ZipArchive();\n$res = $zip->open($zipPath);\nif ($res !== true) {\n    throw new RuntimeException(sprintf('Cannot open %s: ZipArchive error code %d', $zipPath, $res));\n}\n$zip->close();\n(new ZipArchiver($zipPath))->extract($destination);","handlingStrategy":"validation","validationCode":"if (!is_file($zipPath) || !is_readable($zipPath) || filesize($zipPath) < 22) {\n    throw new RuntimeException(\"{$zipPath} is not a readable zip file\");\n}\n$probe = new ZipArchive();\n$res = $probe->open($zipPath);\nif ($res !== true) {\n    throw new RuntimeException(sprintf('ZipArchive refuses %s: error code %d', $zipPath, $res));\n}\n$probe->close();","typeGuard":null,"tryCatchPattern":"try {\n    (new ZipArchiver($path))->extract($destination);\n} catch (RuntimeException $e) {\n    if (str_contains($e->getMessage(), 'Failed to open')) {\n        // corrupt/truncated/unreadable archive — re-download from a trusted source, then retry\n    }\n}","preventionTips":["Verify downloads (size/checksum) before extraction — truncated zips are the top cause","Reject tiny 'zip' files; they are usually HTML error pages from a failed fetch","Open archives once yourself to get the ER_* code when debugging the generic message"],"tags":["grav","zip","corrupt-archive","ziparchive-open","download-truncation"],"backgroundTag":"corrupt-zip-archive","analyzedSha":"6040efed04efa69b8209448ed81308e7c24147c2","analyzedAt":"2026-08-17T05:07:31.593Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}