{"record":{"id":"16815c69047962f4","repo":"schollz/croc","slug":"storage-service-returned-an-invalid-expiration-tim","errorCode":null,"errorMessage":"Storage service returned an invalid expiration time","messagePattern":"Storage service returned an invalid expiration time","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"web/src/protocol/stored.ts","lineNumber":554,"sourceCode":"    }\n  }\n}\n\nasync function completeStoredUpload(\n  created: CreatedStoredUpload,\n  settings: StoredSettings,\n  callbacks: StoredUploadCallbacks,\n  signal?: AbortSignal,\n) {\n  callbacks.onStatus?.(\"Finalizing encrypted transfer…\");\n  const response = await authorizedFetch(\n    api(settings, `/${created.share.id}/complete`),\n    created.uploadToken,\n    { method: \"POST\", signal },\n  );\n  const result = (await response.json()) as { expiresAt: string };\n  if (!Number.isFinite(Date.parse(result.expiresAt))) {\n    throw new Error(\"Storage service returned an invalid expiration time\");\n  }\n  return result.expiresAt;\n}\n\nexport async function uploadStoredFiles(options: {\n  files: StoredPreparedFile[];\n  settings: StoredSettings;\n  downloads?: number;\n  expiresSeconds?: number;\n  callbacks?: StoredUploadCallbacks;\n  signal?: AbortSignal;\n}) {\n  const {\n    files,\n    settings,\n    downloads = 1,\n    expiresSeconds = 24 * 60 * 60,\n    callbacks = {},","sourceCodeStart":536,"sourceCodeEnd":572,"githubUrl":"https://github.com/schollz/croc/blob/e25f1bdc04f07f094d50b0a1bf67e2563944b57a/web/src/protocol/stored.ts#L536-L572","documentation":"completeStoredUpload() finishes the upload by POSTing /complete and reading {expiresAt} from the response. The value must be a parseable date (Number.isFinite(Date.parse(...))); a missing field, empty string, or malformed timestamp throws before the share link is returned, and the upload's finally-block then revokes the transfer.","triggerScenarios":"The /complete response JSON lacks expiresAt or contains a non-ISO string — server bug, or a proxy/error page replacing the JSON while fetch still saw status 2xx.","commonSituations":"Custom or older storage services that return 204 No Content or a different schema for /complete; gateways substituting a captive-portal 200 page; version skew after an API change to the completion response.","solutions":["Update the storage service to return {\"expiresAt\": \"<ISO 8601 timestamp>\"} from POST /transfers/{id}/complete","Check the raw response (network tab) for an unexpected body and fix whatever middleware is intercepting it","Retry after fixing the server — note the client auto-revokes the half-finished transfer, so a fresh upload is required"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { expiresAt = await completeStoredUpload(...); } catch (e) { if (e instanceof Error && e.message.includes(\"expiration\")) { logServerContractViolation(\"expiresAt\"); } throw e; }","preventionTips":["Make POST /transfers/{id}/complete return {\"expiresAt\": \"<ISO 8601>\"} in tests and production","Remember a failed finalize auto-revokes the transfer — fix the server, then upload fresh"],"tags":["server-contract","json","date","stored-transfer"],"backgroundTag":null,"analyzedSha":"e25f1bdc04f07f094d50b0a1bf67e2563944b57a","analyzedAt":"2026-08-15T12:53:39.096Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}