Common causes
- The file is empty or truncated because an upload hit upload_max_filesize/post_max_size or a download failed
- Your code downloaded an HTML error page, redirect or JSON response and saved it as .zip
- The ZIP is opened before the code that writes it has closed the file handle
- The file is a different format (tar.gz, RAR, a theme pack containing nested ZIPs)
- In WordPress, the uploaded ZIP is a full download package from a marketplace rather than the installable plugin ZIP inside it
- A path mix-up means you are opening a different file than the one you think (relative paths, tmp_name vs name)
How to fix it
- Check the return value properly. Compare with === true, and log the integer code when it is not. Code 19 is ER_NOZIP, 11 is ER_OPEN (cannot open), 9 is ER_NOENT (no such file), 5 is ER_READ.
- Inspect the file on disk. Log filesize($path) and the first 4 bytes with bin2hex(file_get_contents($path, false, null, 0, 4)). A ZIP starts with 504b0304; 3c21444f or 3c68746d means HTML was saved instead.
- Fix the download code. With cURL, set CURLOPT_FOLLOWLOCATION to true, check the HTTP status code and Content-Type, and only write the file on a 200 response.
- Close handles before opening. If your script writes the ZIP itself, call fclose() or ZipArchive::close() and clearstatcache() before reopening it.
- Check upload limits. For uploads, check $_FILES['file']['error'] first. A value of 1 or 2 means the file exceeded upload_max_filesize or MAX_FILE_SIZE, so the temp file is incomplete.
- WordPress: upload the right ZIP. Unzip a marketplace download on your computer first; the installable plugin or theme ZIP is usually inside it. Upload that inner ZIP via Plugins > Add New > Upload.
PHP 8
$zip = new ZipArchive();
$res = $zip->open($path);
if ($res !== true) {
$head = bin2hex((string) file_get_contents($path, false, null, 0, 4));
error_log("ZipArchive::open failed: code $res, size " . filesize($path) . ", head $head");
throw new RuntimeException('Not a valid ZIP archive');
}
$zip->extractTo($targetDir);
$zip->close(); How to stop it happening again
- Validate the HTTP status and magic bytes (504b0304) before saving a downloaded file as .zip
- Always check $_FILES[...]['error'] before processing an upload
- Treat ZipArchive::open() as returning a code, never just a truthy value