{"record":{"id":"6859bf4f1d7a332f","repo":"laurent22/joplin","slug":"downloadlimiter","errorCode":"downloadLimiter","errorMessage":"${this.requestId}: Total bytes stored (${this.totalBytes_}) has exceeded the amount established (${this.maxTotalBytes})","messagePattern":"(.+?): Total bytes stored \\((.+?)\\) has exceeded the amount established \\((.+?)\\)","errorType":"exception","errorClass":"JoplinError","httpStatus":null,"severity":"warning","filePath":"packages/lib/downloadController.ts","lineNumber":46,"sourceCode":"\t// counts before the downloaded has finished, so at the end if the totalBytes > maxTotalBytesAllowed\n\t// it means that imageCount will be higher than the total downloaded during the process\n\tprivate imagesCount_ = 0;\n\t// how many images links the content has\n\tprivate imageCountExpected_ = 0;\n\tprivate requestId = '';\n\n\tprivate maxTotalBytes = 0;\n\tpublic readonly maxImagesCount: number;\n\n\tpublic constructor(maxTotalBytes: number, maxImagesCount: number, requestId: string) {\n\t\tthis.maxTotalBytes = maxTotalBytes;\n\t\tthis.maxImagesCount = maxImagesCount;\n\t\tthis.requestId = requestId;\n\t}\n\n\tpublic set totalBytes(value: number) {\n\t\tif (this.totalBytes_ >= this.maxTotalBytes) {\n\t\t\tthrow new JoplinError(`${this.requestId}: Total bytes stored (${this.totalBytes_}) has exceeded the amount established (${this.maxTotalBytes})`, ErrorCode.DownloadLimiter);\n\t\t}\n\t\tthis.totalBytes_ = value;\n\t}\n\n\tpublic get totalBytes() {\n\t\treturn this.totalBytes_;\n\t}\n\n\tpublic set imagesCount(value: number) {\n\t\tif (this.imagesCount_ > this.maxImagesCount) {\n\t\t\tthrow new JoplinError(`${this.requestId}: Total images to be stored (${this.imagesCount_}) has exceeded the amount established (${this.maxImagesCount})`, ErrorCode.DownloadLimiter);\n\t\t}\n\t\tthis.imagesCount_ = value;\n\t}\n\n\tpublic get imagesCount() {\n\t\treturn this.imagesCount_;\n\t}","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/laurent22/joplin/blob/2654b33620775080d1d59c552259d41e33dad3d2/packages/lib/downloadController.ts#L28-L64","documentation":"A JoplinError (ErrorCode.DownloadLimiter) thrown by the totalBytes setter on LimitedDownloadController when the accumulated totalBytes_ has already reached or exceeded maxTotalBytes. It fires on the NEXT write attempt after the limit is hit — the setter checks the previous value before accepting the new one. handleChunk wires this so the in-flight download request is destroyed with the error, halting oversized downloads (used when fetching remote images/resources during sync or preview).","triggerScenarios":"Streaming a remote resource (image, attachment) via a chunked download where the running byte total crosses maxTotalBytes. handleChunk's returned callback adds chunk.length to totalBytes; once totalBytes_ >= maxTotalBytes, the next chunk throws DownloadLimiter and request.destroy(error) aborts the HTTP request.","commonSituations":"Syncing a note whose embedded remote images together exceed the configured byte cap; a malicious or misconfigured server returning an oversized body; legitimate large attachments on a target with a low maxTotalBytes setting.","solutions":["Increase maxTotalBytes when constructing the LimitedDownloadController if large downloads are expected.","Reduce the number/size of remote images in the content being synced/rendered.","Handle ErrorCode.DownloadLimiter at the call site to degrade gracefully (skip the image, show a placeholder) instead of failing the whole operation.","Investigate the source if a single resource unexpectedly exceeds the cap."],"exampleFix":"// before\ncontroller.totalBytes += chunk.length; // throws once cap exceeded\n\n// after — handle the limit and destroy the request cleanly\ntry {\n  controller.totalBytes += chunk.length;\n} catch (error) {\n  request.destroy(error);\n}","handlingStrategy":"try-catch","validationCode":"// Check the remaining budget before starting a download\nif (controller.totalBytes + expectedSize > controller.maxTotalBytes) {\n  // skip the download or raise the limit\n  return;\n}","typeGuard":"import JoplinError from './JoplinError';\nimport { ErrorCode } from './errors';\nfunction isDownloadLimitError(e: unknown): e is JoplinError {\n  return e instanceof JoplinError && (e as any).code === ErrorCode.DownloadLimiter;\n}","tryCatchPattern":"try {\n  controller.totalBytes += chunk.length;\n} catch (error) {\n  if (isDownloadLimitError(error)) {\n    request.destroy(error); // abort the HTTP download cleanly\n    return;\n  }\n  throw error;\n}","preventionTips":["Size maxTotalBytes to the largest legitimate resource you expect to download.","Use the handleChunk() helper so the request is destroyed automatically on limit breach.","Catch ErrorCode.DownloadLimiter at the operation level to skip oversized items gracefully."],"tags":["download","rate-limiting","resources","sync","memory"],"backgroundTag":null,"analyzedSha":"2654b33620775080d1d59c552259d41e33dad3d2","analyzedAt":"2026-08-12T14:26:46.263Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}