schollz/croc · warning
expired or invalid stored-upload receipt
Error message
expired or invalid stored-upload receipt
What it means
Thrown in App.tsx when restoring a persisted stored-upload receipt: it must contain browserURL and uploadToken, and a parseable expiresAt strictly in the future (expiresAt.getTime() > Date.now()). Any missing field, unparseable date (including the empty-string default), or elapsed expiry throws.
Source
Thrown at web/src/App.tsx:286
const storageKey = sessionStorage.key(index);
if (!storageKey?.startsWith("croc-store-upload:")) continue;
try {
const raw = sessionStorage.getItem(storageKey);
if (!raw) continue;
const receipt = JSON.parse(raw) as {
browserURL?: string;
uploadToken?: string;
expiresAt?: string;
downloads?: number;
};
const expiresAt = new Date(receipt.expiresAt ?? "");
if (
!receipt.browserURL ||
!receipt.uploadToken ||
!Number.isFinite(expiresAt.getTime()) ||
expiresAt.getTime() <= Date.now()
) {
throw new Error("expired or invalid stored-upload receipt");
}
const share = parseStoredShare(receipt.browserURL);
const candidate: StoredUploadResult = {
share,
uploadToken: receipt.uploadToken,
expiresAt: expiresAt.toISOString(),
browserURL: receipt.browserURL,
cliToken: formatStoredCLIToken(share),
downloads:
Number.isSafeInteger(receipt.downloads) &&
(receipt.downloads ?? 0) > 0
? receipt.downloads!
: 1,
};
if (
!restored ||
new Date(candidate.expiresAt).getTime() >
new Date(restored.expiresAt).getTime()View on GitHub (pinned to e25f1bdc04)
Solutions
- Catch this error during restore and prune the receipt from storage instead of surfacing a crash
- Write receipts as one atomic setItem of fully-built JSON so partial writes cannot persist
- On suspected clock skew, compare against server time before declaring expiry
Example fix
// before
const receipt = JSON.parse(raw);
validateReceipt(receipt); // throws on stale receipt, breaks app load
// after
let receipt;
try {
receipt = parseStoredReceipt(raw);
} catch {
localStorage.removeItem(key); // stale/corrupt receipt: prune silently
} Defensive patterns
Strategy: try-catch
Validate before calling
function isViableReceipt(r: unknown): boolean {
const x = r as Record<string, unknown>;
return typeof x?.browserURL === 'string' && x.browserURL !== '' &&
typeof x?.uploadToken === 'string' && x.uploadToken !== '' &&
typeof x?.expiresAt === 'string' && Number.isFinite(Date.parse(x.expiresAt)) &&
Date.parse(x.expiresAt) > Date.now();
} Type guard
function isRestorableReceipt(r: unknown): r is { browserURL: string; uploadToken: string; expiresAt: string } {
return isViableReceipt(r);
} Try / catch
try { receipt = parseStoredReceipt(raw); } catch (e) { if (e instanceof Error && e.message === 'expired or invalid stored-upload receipt') { localStorage.removeItem(key); receipt = null; } else throw e; } Prevention
- Prune expired receipts on a schedule, not only at restore time
- Write receipts as one atomic setItem of complete JSON after upload finishes
When it happens
Trigger: Re-opening the app after a stored upload's expiry window passed (default 24h); a receipt written by an older app version lacking uploadToken/browserURL; a corrupted or hand-edited receipt string in localStorage.
Common situations: User returns the next day after default expiry; schema-migration leftovers in storage; device clock skew making valid receipts look expired; private-mode storage writing partial JSON.
Related errors
- The stored file failed SHA-256 verification (${hex(actual)}
- Invalid stored-transfer id
- Stored transfers can contain at most ${settings.maxFiles} fi
- Stored transfer exceeds the ${settings.maxTransferBytes} byt
- Duplicate filename: ${name}
AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15).
Data as JSON: /api/errors/f4862ec0fe049de3.
Report an issue: GitHub.