Leantime/leantime · error · Exception

notification.plugin_zip_invalid_arg

Error message

notification.plugin_zip_invalid_arg

What it means

The match maps ZipArchive::ER_INVAL to 'Zip: Invalid argument'. libzip returns ER_INVAL for an invalid filename/flags combination or when the archive contains invalid entries. Here the filename handed to open() is derived by naive substr() parsing of the Content-Disposition header (everything after 'filename='), so a missing, empty, or malformed header produces an empty or bogus path like '/tmp/' and open() fails with ER_INVAL.

Source

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

        }

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

View on GitHub (pinned to 9a9f49f100)

Solutions

  1. curl -sD - -o /dev/null the download endpoint (with license headers) and inspect the exact Content-Disposition value.
  2. Patch the parsing to a regex that handles quotes and trailing parameters (see exampleFix) and fails loudly on absence.
  3. If the header is genuinely absent server-side, report it - the plugin cannot be installed under this Leantime version otherwise.
  4. Clear any stale /tmp file created from a previous malformed attempt.

Example fix

// before
$filename = $response->header('Content-Disposition');
$filename = substr($filename, strpos($filename, 'filename=') + 9);

// after: regex that handles quotes/parameters and fails loudly when absent
if (! preg_match('/filename="?([^";]+)\.zip"?/i', (string) $response->header('Content-Disposition'), $m)) {
    throw new \Exception('Malformed Content-Disposition header from marketplace');
}
$filename = $m[1];
Defensive patterns

Strategy: validation

Validate before calling

if (! preg_match('/filename="?([^";]+\.zip)"?/i', (string) $response->header('Content-Disposition'), $m)) {
    throw new RuntimeException('Marketplace response lacks a parsable filename - cannot install');
}
$foldername = Str::studly(basename($m[1], '.zip'));

Prevention

When it happens

Trigger: installMarketplacePlugin() where the marketplace response's Content-Disposition header lacks 'filename=' (substr yields '' -> path '/tmp/'), contains quotes/semicolons the substr() logic does not strip, or carries path separators; alternatively the zip contains entries with invalid names.

Common situations: Marketplace API or CDN changing its Content-Disposition format; a proxy stripping the header; header value like 'attachment; filename="Name.zip"; size=123' leaving trailing quotes/params in the parsed name.

Related errors


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