Leantime/leantime · error · Exception

notification.plugin_zip_unknown_err

Error message

notification.plugin_zip_unknown_err

What it means

The default arm of the match over ZipArchive::open(): any return code not explicitly mapped becomes 'Zip: Unknown error'. The unmapped libzip codes include the most instructive ones: ER_TMPOPEN (libzip could not create its own temp copy - /tmp full or unwritable), ER_EOF (premature end of file), ER_CRC (checksum mismatch), ER_ZLIB (zlib unavailable/mismatch), ER_COMPNOTSUPP/ER_ENCRNOTSUPP (unsupported compression or an encrypted zip), ER_INTERNAL. Because the code discards the actual code, the real cause is invisible without instrumentation.

Source

Thrown at app/Domain/Plugins/Services/Plugins.php:744

        }

        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);

        if (! $this->pluginRepository->addPlugin($pluginModel)) {
            throw new \Exception(__('notification_cant_add_to_db'));
        }
    }

View on GitHub (pinned to 9a9f49f100)

Solutions

  1. Capture the actual code first: log the return value of $zip->open($temporaryFile) (see exampleFix) - it maps 1:1 to a ZipArchive::ER_* constant.
  2. Free temp space: libzip duplicates the archive, so /tmp needs about 2x the zip size.
  3. Verify the zip extension is complete: php -m | grep -e zip -e zlib; reinstall php-zip/php-zlib packages if not.
  4. Test the same file with unzip -t on the server to distinguish artifact corruption from environment limits.
  5. If the artifact is encrypted or uses unsupported compression, report it - Leantime's installer cannot extract it.

Example fix

// before
match ($zip->open($temporaryFile)) {
    // ... mapped constants ...
    default => throw new \Exception(__('notification.plugin_zip_unknown_err')),
    true => null,
};

// after: keep the code in the message so the unmapped ER_* is diagnosable
$code = $zip->open($temporaryFile);
if ($code !== true) {
    throw new \Exception(__('notification.plugin_zip_unknown_err')." (ZipArchive::open code {$code})");
}
Defensive patterns

Strategy: try-catch

Validate before calling

// capture the code and map the constants the installer forgot
$code = $zip->open($temporaryFile);
if ($code !== true) {
    $known = [ZipArchive::ER_TMPOPEN => 'temp create failed (tmp full?)', ZipArchive::ER_EOF => 'premature EOF', ZipArchive::ER_CRC => 'CRC mismatch', ZipArchive::ER_ZLIB => 'zlib unavailable', ZipArchive::ER_ENCRNOTSUPP => 'encrypted zip unsupported'];
    throw new RuntimeException($known[$code] ?? "ZipArchive::open code {$code}");
}

Try / catch

try {
    $plugins->installMarketplacePlugin($plugin, $version);
} catch (\Exception $e) {
    Log::error('Plugin zip open failed: '.$e->getMessage().' | tmp free: '.disk_free_space(sys_get_temp_dir()));
    return back()->with('error', 'Plugin archive could not be opened - check /tmp space and the zip extension.');
}

Prevention

When it happens

Trigger: installMarketplacePlugin() where open() returns ER_TMPOPEN (needs roughly one extra temp copy of the archive - /tmp exhausted), ER_ENCRNOTSUPP (password-protected zip), ER_ZLIB (PHP zip extension without zlib), ER_EOF/ER_CRC (truncated or damaged body), or any newer libzip code the match does not know.

Common situations: Tight /tmp quota - the downloaded zip plus libzip's internal copy exceed it; marketplace shipping an encrypted or unusually compressed artifact; PHP builds with a minimal zip extension; libzip version differences introducing unmapped codes.

Related errors


AI-assisted analysis of Leantime/leantime@9a9f49f100 (2026-08-21). Data as JSON: /api/errors/78826162fd9f255c. Report an issue: GitHub.