santifer/career-ops · error · Error

Apify run ${runId} returned non-array dataset payload

Error message

Apify run ${runId} returned non-array dataset payload

What it means

Thrown by `fetchDatasetItems` (plugins/apify/_apify.mjs:182) when the GET to `/actor-runs/<runId>/dataset/items` returns a payload that is not a JSON array. Apify's dataset endpoint is expected to return a bare JSON array of item objects; a non-array (object envelope, paginated wrapper, empty object, or error body) means the result cannot be consumed as items, so it fails rather than silently treating it as zero results.

Source

Thrown at plugins/apify/_apify.mjs:182

    if (sleepMs > 0) await sleep(sleepMs);
  }
  // Fire-and-forget cleanup; don't add abortRun's 5s to our wall-clock budget.
  void abortRun(runId, token).catch(() => {});
  const suffix = lastError ? ` (last error: ${lastError.message})` : '';
  throw new Error(`Apify run ${runId} did not finish within ${Math.round(timeoutMs / 1000)}s${suffix}`);
}

async function fetchDatasetItems(runId, token, deadline = null) {
  const url = `${APIFY_API_BASE}/actor-runs/${runId}/dataset/items`;
  const items = await fetchJson(
    url,
    { headers: authHeaders(token) },
    PER_REQUEST_TIMEOUT_MS * 2,
    CONNECT_RETRY_ATTEMPTS,
    deadline,
  );
  if (!Array.isArray(items)) {
    throw new Error(`Apify run ${runId} returned non-array dataset payload`);
  }
  return items;
}

export async function runActor(actorId, input, { timeoutMs = DEFAULT_RUN_TIMEOUT_MS, token = process.env.APIFY_TOKEN } = {}) {
  if (!token) throw new Error('APIFY_TOKEN not set');
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
    throw new Error(`apify: invalid timeoutMs ${JSON.stringify(timeoutMs)} (must be a positive finite number of milliseconds)`);
  }
  // Single deadline shared across startRun → waitForRun → fetchDatasetItems so
  // the caller's timeoutMs is the end-to-end ceiling, not just the wait loop.
  const deadline = Date.now() + timeoutMs;
  const runId = await startRun(actorId, input, token, deadline);
  const run = await waitForRun(runId, token, deadline, timeoutMs);
  if (run.status !== 'SUCCEEDED') {
    const reason = run.statusMessage ? `: ${run.statusMessage}` : '';
    throw new Error(`Apify actor ${actorId} finished with status ${run.status}${reason}`);
  }

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Inspect the actor run on the Apify console — confirm the dataset actually has items and the expected shape.
  2. Check for Apify v2 API changes to the dataset/items endpoint (envelope vs bare array).
  3. Retry once; if a proxy is intercepting, bypass it for api.apify.com.
  4. If the API schema changed, update fetchDatasetItems to unwrap the envelope.
Defensive patterns

Strategy: try-catch

Try / catch

try {
  const items = await runActor(actorId, input, opts);
} catch (err) {
  if (/non-array dataset payload/.test(err.message)) {
    console.error(`Apify dataset shape changed for ${actorId} — possible API drift. ${err.message}`);
    // do not blindly coerce; investigate the actual payload
  } else throw err;
}

Prevention

When it happens

Trigger: After a SUCCEEDED run, the dataset fetch returns something like `{ data: [...] }` (envelope), a paginated object, or an error JSON object. `Array.isArray(items)` is false, so the guard throws.

Common situations: Apify API version drift returning an envelope instead of a bare array; a dataset endpoint change to pagination by default; an intercepting proxy/captive portal returning a JSON error object; the run's dataset being empty but returned as `{}` instead of `[]`.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/b2729b4610d17ea1. Report an issue: GitHub.