Leantime/leantime · error · Exception

notification.plugin_cant_download

Error message

notification.plugin_cant_download

What it means

Inside installMarketplacePlugin(), the zip body was already fetched successfully (HTTP 200, Content-Type application/zip) and the filename was derived from the Content-Disposition header. The error fires when file_put_contents() to sys_get_temp_dir()/{Folder}.zip returns falsy - i.e., the download itself worked but writing the local temp file failed. The translated message is 'Could not download plugin'.

Source

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

            throw new RequestException($response);
        }

        if ($response->header('Content-Type') !== 'application/zip') {
            throw new RequestException($response);
        }

        $filename = $response->header('Content-Disposition');
        $filename = substr($filename, strpos($filename, 'filename=') + 9);
        $foldername = Str::studly(basename($filename, '.zip'));
        $filename = Str::finish($foldername, '.zip');

        if (
            ! file_put_contents(
                $temporaryFile = Str::finish(sys_get_temp_dir(), '/').$filename,
                $response->body()
            )
        ) {
            throw new \Exception(__('notification.plugin_cant_download'));
        }

        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')),

View on GitHub (pinned to 9a9f49f100)

Solutions

  1. php -r 'var_dump(is_writable(sys_get_temp_dir()));' under the web SAPI - if false, fix permissions or set sys_temp_dir to a writable path.
  2. Check open_basedir in php.ini / .user.ini includes the temp directory.
  3. df -h <tempdir> and free space (the zip plus libzip's temp copy need roughly 2x the archive size).
  4. Mount a writable /tmp (docker-compose: tmpfs: with adequate size, or remove :ro).
  5. If Content-Disposition is malformed (empty filename), report the marketplace header issue and patch the filename parsing.
Defensive patterns

Strategy: retry

Validate before calling

$tmpDir = sys_get_temp_dir();
if (! is_writable($tmpDir)) {
    throw new RuntimeException("Temp dir {$tmpDir} is not writable - fix sys_temp_dir/open_basedir before installing plugins");
}
if (disk_free_space($tmpDir) < $expectedZipBytes * 2) {
    throw new RuntimeException('Not enough temp space for plugin download + extraction');
}

Try / catch

retry: install attempts (1..3) => {
    try {
        return $plugins->installMarketplacePlugin($plugin, $version);
    } catch (\Exception $e) {
        if ($e->getMessage() !== __('notification.plugin_cant_download') || $installAttempts === 3) {
            throw $e;
        }
        usleep(500_000 * $installAttempts); // temp-write failures are often transient
    }
}

Prevention

When it happens

Trigger: installMarketplacePlugin() where: the temp directory is not writable (open_basedir restriction, read-only /tmp in a hardened container); the disk/tmpfs backing /tmp is full; the parsed filename is empty because Content-Disposition lacked 'filename=' so the target path degenerates to the directory '/tmp/'; a path collision with an existing directory of the same name.

Common situations: Docker/Kubernetes images mounting /tmp read-only or with a tiny tmpfs size limit; shared hosting where open_basedir excludes the system temp dir; full disk during large plugin downloads; sys_temp_dir pointed somewhere odd via PHP config.

Related errors


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