schollz/croc · error

Storage service returned an invalid expiration time

Error message

Storage service returned an invalid expiration time

What it means

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.

Source

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

    }
  }
}

async function completeStoredUpload(
  created: CreatedStoredUpload,
  settings: StoredSettings,
  callbacks: StoredUploadCallbacks,
  signal?: AbortSignal,
) {
  callbacks.onStatus?.("Finalizing encrypted transfer…");
  const response = await authorizedFetch(
    api(settings, `/${created.share.id}/complete`),
    created.uploadToken,
    { method: "POST", signal },
  );
  const result = (await response.json()) as { expiresAt: string };
  if (!Number.isFinite(Date.parse(result.expiresAt))) {
    throw new Error("Storage service returned an invalid expiration time");
  }
  return result.expiresAt;
}

export async function uploadStoredFiles(options: {
  files: StoredPreparedFile[];
  settings: StoredSettings;
  downloads?: number;
  expiresSeconds?: number;
  callbacks?: StoredUploadCallbacks;
  signal?: AbortSignal;
}) {
  const {
    files,
    settings,
    downloads = 1,
    expiresSeconds = 24 * 60 * 60,
    callbacks = {},

View on GitHub (pinned to e25f1bdc04)

Solutions

  1. Update the storage service to return {"expiresAt": "<ISO 8601 timestamp>"} from POST /transfers/{id}/complete
  2. Check the raw response (network tab) for an unexpected body and fix whatever middleware is intercepting it
  3. Retry after fixing the server — note the client auto-revokes the half-finished transfer, so a fresh upload is required
Defensive patterns

Strategy: try-catch

Try / catch

try { expiresAt = await completeStoredUpload(...); } catch (e) { if (e instanceof Error && e.message.includes("expiration")) { logServerContractViolation("expiresAt"); } throw e; }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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