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
- 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).
- Ensure `transfer.cancel()` is the only path that sets the cancelled flag, so the error reliably maps to a user action.
- Clean up any partial files/directories created before cancellation if your UX requires it.
- 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
- Treat 'Download cancelled' as a clean abort; never auto-retry.
- Ensure only transfer.cancel() flips the cancelled flag so the error reliably maps to a user action.
- Clean up partial files/dirs created before cancellation when your UX requires it.
- Check isCancelled() at the top of each loop iteration (as the code does) to stop promptly.
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.