{"record":{"id":"c53d4bb729d9567d","repo":"schollz/croc","slug":"storage-service-returned-an-invalid-download-count","errorCode":null,"errorMessage":"Storage service returned an invalid download count","messagePattern":"Storage service returned an invalid download count","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"web/src/protocol/stored.ts","lineNumber":451,"sourceCode":"  const created = (await response.json()) as {\n    id: string;\n    uploadToken: string;\n    uploadExpiresAt: string;\n    chunkSize: number;\n  };\n  if (created.chunkSize !== storedChunkSize) {\n    throw new Error(\n      `Storage service returned unsupported chunk size ${created.chunkSize}`,\n    );\n  }\n  if (!isCapability(created.uploadToken)) {\n    throw new Error(\"Storage service returned an invalid upload capability\");\n  }\n  const downloadsHeader = response.headers.get(\"X-Croc-Downloads\");\n  const acceptedDownloads =\n    downloadsHeader === null ? 1 : Number(downloadsHeader);\n  if (!Number.isSafeInteger(acceptedDownloads) || acceptedDownloads < 1) {\n    throw new Error(\"Storage service returned an invalid download count\");\n  }\n  if (acceptedDownloads !== downloads) {\n    throw new Error(\n      `Storage service created ${acceptedDownloads} downloads instead of ${downloads}`,\n    );\n  }\n  return {\n    share: validateShare({\n      origin: window.location.origin,\n      id: created.id,\n      key,\n    }),\n    uploadToken: created.uploadToken,\n  };\n}\n\nasync function uploadStoredManifest(\n  plan: StoredUploadPlan,","sourceCodeStart":433,"sourceCodeEnd":469,"githubUrl":"https://github.com/schollz/croc/blob/e25f1bdc04f07f094d50b0a1bf67e2563944b57a/web/src/protocol/stored.ts#L433-L469","documentation":"After creation, the client reads the X-Croc-Downloads response header to learn how many downloads the server actually granted; the value must be an integer >= 1 (a missing header defaults to 1). A non-numeric, fractional, zero, or negative value throws, because the download budget drives claim/commit accounting.","triggerScenarios":"The create response's X-Croc-Downloads header is e.g. \"0\", \"1.5\", \"many\", or garbage — a misimplemented server, or an HTTP/1.0-style proxy that drops or mangles headers (dropped header is fine: it defaults to 1; a corrupted one throws).","commonSituations":"Self-hosted services that clamp downloads to 0 when storage is tight; debugging proxies rewriting headers; custom server implementations that never set the header but also use a default other than 1.","solutions":["Fix the server to send X-Croc-Downloads: <requested positive integer> or omit it entirely","Check for proxies (corporate gateways, devtools rewrite rules) corrupting the header","Retry against a known-good deployment to confirm it is a server-side contract violation"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { return await createStoredUpload(...); } catch (e) { if (e instanceof Error && e.message.includes(\"invalid download count\")) { logServerContractViolation(\"X-Croc-Downloads\"); } throw e; }","preventionTips":["Ensure the server sends X-Croc-Downloads as a positive integer or omits it (defaults to 1)","Keep header-rewriting proxies off the storeAPI path"],"tags":["server-contract","header","validation","stored-transfer"],"backgroundTag":null,"analyzedSha":"e25f1bdc04f07f094d50b0a1bf67e2563944b57a","analyzedAt":"2026-08-15T12:53:39.096Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}