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

  1. Catch this error during restore and prune the receipt from storage instead of surfacing a crash
  2. Write receipts as one atomic setItem of fully-built JSON so partial writes cannot persist
  3. 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

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


AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15). Data as JSON: /api/errors/f4862ec0fe049de3. Report an issue: GitHub.