schollz/croc · error

Stored-transfer expiration must be a whole number of seconds

Error message

Stored-transfer expiration must be a whole number of seconds of at least one minute

What it means

Thrown by uploadStoredFiles() (stored.ts:583) when `expiresSeconds` is not a safe integer, is under 60 (one minute), or exceeds 9,223,372,036 seconds (~292 years, the int64 ceiling the backend accepts). The service stores expiry as whole seconds in an int64, so fractional, sub-minute, or astronomically large values are rejected before the upload starts.

Source

Thrown at web/src/protocol/stored.ts:588

    downloads = 1,
    expiresSeconds = 24 * 60 * 60,
    callbacks = {},
    signal,
  } = options;
  if (!Number.isSafeInteger(downloads) || downloads < 1) {
    throw new Error("Stored-transfer downloads must be a positive integer");
  }
  if (downloads > settings.maxDownloads) {
    throw new Error(
      `Stored transfers can allow at most ${settings.maxDownloads} downloads`,
    );
  }
  if (
    !Number.isSafeInteger(expiresSeconds) ||
    expiresSeconds < 60 ||
    expiresSeconds > 9_223_372_036
  ) {
    throw new Error(
      "Stored-transfer expiration must be a whole number of seconds of at least one minute",
    );
  }
  if (
    settings.maxExpiresSeconds > 0 &&
    expiresSeconds > settings.maxExpiresSeconds
  ) {
    throw new Error(
      `Stored transfers can expire after at most ${settings.maxExpiresSeconds} seconds`,
    );
  }
  const key = await wasm().storeGenerateKey();
  const plan = planStoredUpload(files);
  const created = await createStoredUpload(
    key,
    plan,
    downloads,
    expiresSeconds,

View on GitHub (pinned to e25f1bdc04)

Solutions

  1. Round and floor at 60: expiresSeconds = Math.max(60, Math.floor(v)), then re-check Number.isSafeInteger
  2. Compute expiry from integer units (integer hours * 3600)
  3. Coerce possible strings with Number() before validating

Example fix

// before
await uploadStoredFiles({ files, settings, expiresSeconds: minutes * 60 });

// after
const expiresSeconds = Math.max(60, Math.round(minutes) * 60);
await uploadStoredFiles({ files, settings, expiresSeconds });
Defensive patterns

Strategy: validation

Validate before calling

const expiresSeconds = Math.round(Number(raw));
if (!Number.isSafeInteger(expiresSeconds) || expiresSeconds < 60 || expiresSeconds > 9_223_372_036) {
  throw new RangeError('expiration must be whole seconds in [60, 9223372036]');
}

Type guard

function isValidExpiration(v: unknown): v is number {
  return typeof v === 'number' && Number.isSafeInteger(v) && v >= 60 && v <= 9_223_372_036;
}

Try / catch

try { await uploadStoredFiles(opts); } catch (e) { if (e instanceof Error && e.message.includes('whole number of seconds')) { flagField('expiration'); return; } throw e; }

Prevention

When it happens

Trigger: Calling uploadStoredFiles({ expiresSeconds: 30 }), with a fractional value like 86400.5, NaN, Infinity, or anything above 9_223_372_036. Also when expiry is computed from a float (fractional minutes * 60) or passed as a string ('86400' fails Number.isSafeInteger).

Common situations: UI permits '0 minutes'; expiry derived from a millisecond Date.now() delta without rounding; values deserialized from JSON config left as strings.

Related errors


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