{"record":{"id":"9e32eebda83b6022","repo":"schollz/croc","slug":"storage-service-returned-an-invalid-upload-capabil","errorCode":null,"errorMessage":"Storage service returned an invalid upload capability","messagePattern":"Storage service returned an invalid upload capability","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"web/src/protocol/stored.ts","lineNumber":445,"sourceCode":"      plaintextBytes: plan.totalSize,\n      ...(downloads === 1 ? {} : { downloads }),\n      ...(expiresSeconds === 24 * 60 * 60 ? {} : { expiresSeconds }),\n    }),\n  });\n  if (!response.ok) throw await responseError(response);\n  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    }),","sourceCodeStart":427,"sourceCodeEnd":463,"githubUrl":"https://github.com/schollz/croc/blob/e25f1bdc04f07f094d50b0a1bf67e2563944b57a/web/src/protocol/stored.ts#L427-L463","documentation":"The upload token returned by the create endpoint must be a capability: exactly 32 bytes of opaque data in canonical unpadded base64url (isCapability() round-trips the encoding to reject padded or non-canonical forms). A missing, wrong-length, or non-canonical uploadToken throws before any bytes are uploaded.","triggerScenarios":"created.uploadToken is undefined/short/padded or not valid base64url — a server implementation that returns a JWT or session string instead of the 32-byte capability, or a proxy that rewrites the JSON body.","commonSituations":"Third-party / incompatible storage backends; API gateways that decode/re-encode response bodies; version skew where the token format changed.","solutions":["Run a matching-version storage service that returns a 32-byte base64url capability as uploadToken","Remove any middleware that rewrites or re-serializes the /transfers response body","Inspect the raw create response (curl) to see what uploadToken actually contains"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { return await createStoredUpload(...); } catch (e) { if (e instanceof Error && e.message.includes(\"upload capability\")) { logServerContractViolation(\"uploadToken\"); } throw e; }","preventionTips":["Verify your storage service returns a 32-byte base64url capability for uploadToken","Avoid middleboxes that re-encode response bodies on /transfers endpoints"],"tags":["server-contract","token","capability","stored-transfer"],"backgroundTag":null,"analyzedSha":"e25f1bdc04f07f094d50b0a1bf67e2563944b57a","analyzedAt":"2026-08-15T12:53:39.096Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}