Leantime/leantime · error · Exception
notification.plugin_zip_not_zip
Error message
notification.plugin_zip_not_zip
What it means
The match maps ZipArchive::ER_NOZIP to 'Zip: Not a zip archive'. The temp file exists and opened, but it has no zip magic - libzip could not find a zip structure at all. The earlier Content-Type === 'application/zip' check (Plugins.php:703) only validates a header, so a server can still send a non-zip body with that header.
Source
Thrown at app/Domain/Plugins/Services/Plugins.php:740
is_dir($pluginDir = "{$this->pluginDirectory}{$foldername}")
&& ! File::deleteDirectory($pluginDir)
) {
throw new \Exception(__('notification.plugin_cant_remove'));
}
if (! mkdir($pluginDir) && ! is_dir($pluginDir)) {
throw new \RuntimeException(sprintf('Directory "%s" was not created', $pluginDir));
}
$zip = new \ZipArchive;
match ($zip->open($temporaryFile)) {
\ZipArchive::ER_EXISTS => throw new \Exception(__('notification.plugin_zip_exists')),
\ZipArchive::ER_INCONS => throw new \Exception(__('notification.plugin_zip_inconsistent')),
\ZipArchive::ER_INVAL => throw new \Exception(__('notification.plugin_zip_invalid_arg')),
\ZipArchive::ER_MEMORY => throw new \Exception(__('notification.plugin_zip_malloc')),
\ZipArchive::ER_NOENT => throw new \Exception(__('notification.plugin_zip_no_file')),
\ZipArchive::ER_NOZIP => throw new \Exception(__('notification.plugin_zip_not_zip')),
\ZipArchive::ER_OPEN => throw new \Exception(__('notification.plugin_zip_cant_open')),
\ZipArchive::ER_READ => throw new \Exception(__('notification.plugin_zip_read_err')),
\ZipArchive::ER_SEEK => throw new \Exception(__('notification.plugin_zip_seek_err')),
default => throw new \Exception(__('notification.plugin_zip_unknown_err')),
true => null,
};
if (! $zip->extractTo($pluginDir)) {
throw new \Exception(__('notification.plugin_zip_cant_extract'));
}
$zip->close();
unlink($temporaryFile);
// read the composer.json content from the plugin phar file
$pluginModel = $this->createPluginFromComposer($foldername, $plugin->license);
View on GitHub (pinned to 9a9f49f100)
Solutions
- Inspect the temp file: head -c 200 /tmp/{Folder}.zip - if it starts with '<' or '{', the body is an error page, not a zip.
- Validate the plugin license key and marketplace account status, then retry.
- Bypass/verify any corporate proxy for marketplace.leantime.io.
- Report to Leantime if the artifact endpoint serves non-zip bodies with 200 + application/zip.
Defensive patterns
Strategy: validation
Validate before calling
$body = $response->body();
if (strncmp($body, "PK\x03\x04", 4) !== 0) {
throw new RuntimeException('Marketplace returned a non-zip body (license/proxy error page?)');
} Type guard
/** True when $path points at a file starting with the zip magic bytes. */
function looksLikeZip(string $path): bool
{
$fh = @fopen($path, 'rb');
if (! $fh) {
return false;
}
$magic = fread($fh, 4);
fclose($fh);
return $magic === "PK\x03\x04" || $magic === "PK\x05\x06" || $magic === "PK\x07\x08";
} Prevention
- Check the zip magic bytes before opening - a Content-Type header alone proves nothing.
- Keep the marketplace license key valid; soft-error pages arrive with HTTP 200.
- Route marketplace traffic without interfering proxies.
When it happens
Trigger: installMarketplacePlugin() where the 200/zip-labeled body is actually HTML or JSON: a marketplace soft-error page (expired license, rate limit) returned with HTTP 200; a proxy/captive portal injecting an HTML page; the identifier/version hitting an error route that still sets application/zip.
Common situations: Invalid or exhausted license key producing an HTML error page with a 200 status; marketplace API changes; transparent proxies on corporate networks rewriting responses; the marketplace serving a JSON error object.
Related errors
- notification.plugin_zip_exists
- notification.plugin_zip_inconsistent
- notification.plugin_zip_malloc
- notification.plugin_zip_unknown_err
- notification.plugin_cant_download
AI-assisted analysis of Leantime/leantime@9a9f49f100 (2026-08-21).
Data as JSON: /api/errors/5220454c0ff0ebd9.
Report an issue: GitHub.