Eugeny/tabby · info · Error

Download cancelled

Error message

Download cancelled

What it means

Thrown inside `downloadFolderRecursive` when the in-progress `DirectoryDownload` transfer reports it has been cancelled (`transfer.isCancelled()`) partway through iterating remote directory entries. It aborts the recursive walk immediately rather than continuing to download files into a transfer the user has stopped.

Source

Thrown at tabby-ssh/src/components/sftpPanel.component.ts:301

        let totalSize = 0
        const items = await this.sftp.readdir(folder.fullPath)
        for (const item of items) {
            if (item.isDirectory) {
                totalSize += await this.calculateFolderSizeAndUpdate(item, transfer)
            } else {
                totalSize += item.size
            }
            transfer.setTotalSize(totalSize)
        }
        return totalSize
    }

    private async downloadFolderRecursive (folder: SFTPFile, transfer: DirectoryDownload, relativePath: string): Promise<void> {
        const items = await this.sftp.readdir(folder.fullPath)

        for (const item of items) {
            if (transfer.isCancelled()) {
                throw new Error('Download cancelled')
            }

            const itemRelativePath = relativePath ? `${relativePath}/${item.name}` : item.name

            transfer.setStatus(itemRelativePath)
            if (item.isDirectory) {
                await transfer.createDirectory(itemRelativePath)
                await this.downloadFolderRecursive(item, transfer, itemRelativePath)
            } else {
                const fileDownload = await transfer.createFile(itemRelativePath, item.mode, item.size)
                await this.sftp.download(item.fullPath, fileDownload)
            }
        }
    }

    getModeString (item: SFTPFile): string {
        const s = 'SGdrwxrwxrwx'
        const e = '   ---------'

View on GitHub (pinned to 14e2d60b9b)

Solutions

  1. Catch 'Download cancelled' in the caller of `downloadFolderRecursive` and treat it as a clean abort (close partial files, stop the progress UI, do not show an error toast).
  2. Ensure `transfer.cancel()` is the only path that sets the cancelled flag, so the error reliably maps to a user action.
  3. Clean up any partial files/directories created before cancellation if your UX requires it.
  4. Avoid auto-retrying a cancelled download; the user explicitly stopped it.

Example fix

// before
for (const item of items) {
    if (transfer.isCancelled()) throw new Error('Download cancelled')
    ...
}

// caller pattern
try { await this.downloadFolderRecursive(folder, transfer, '') }
catch (e) {
    if (e instanceof Error && e.message === 'Download cancelled') {
        await transfer.cleanup(); return  // user-initiated abort
    }
    throw e
}
Defensive patterns

Strategy: try-catch

Type guard

function isCancelledTransfer (t: DirectoryDownload): boolean { return t.isCancelled() }

Try / catch

try {
    await this.downloadFolderRecursive(folder, transfer, '')
} catch (e) {
    if (e instanceof Error && e.message === 'Download cancelled') {
        await transfer.cleanup()  // user-initiated; clean up partial files
        return
    }
    throw e
}

Prevention

When it happens

Trigger: User clicks Cancel on the folder-download progress UI during an SFTP recursive directory download; `isCancelled()` flips true and the next loop iteration in `downloadFolderRecursive` throws before reading/copying the next item.

Common situations: Large directory download the user aborted; user changed their mind after a few files; network hiccup made the user cancel and retry with a smaller selection.

Related errors


AI-assisted analysis of Eugeny/tabby@14e2d60b9b (2026-08-12). Data as JSON: /api/errors/386868ebe7024af1. Report an issue: GitHub.