santifer/career-ops · error · Error
Apify run returned non-array dataset payload
Error message
Apify run ${runId} returned non-array dataset payload What it means
fetchDatasetItems fetches dataset items for a completed Apify run via the Apify API. The library expects the API (or a retry-wrapped fetch helper) to return an array of items; if the parsed payload is not an array, it throws this error to signal an unexpected response shape rather than passing garbage downstream.
Solutions
- Re-run the actor and verify it produces a default dataset with items (check run detail in Apify console).
- Verify APIFY_TOKEN is valid and not rate-limited; a 401/429 body parsed as JSON is not an array.
- Log the raw payload before this point to see the actual shape (e.g. {data:{items:[...]}}).
- Check for Apify API version/schema changes and update the plugin's dataset fetch to unwrap the correct field.
- Wrap the call in try/catch and fall back to a direct GET of runId's dataset endpoint with ?clean=true.
Example fix
// before const items = await res.json(); return items; // after const payload = await res.json(); const items = Array.isArray(payload) ? payload : (Array.isArray(payload?.data?.items) ? payload.data.items : []);
Defensive patterns
Strategy: type-guard
Validate before calling
const items = await fetchDatasetItems(runId, token, deadline);
if (!Array.isArray(items)) throw new TypeError('dataset payload is not an array'); Type guard
function isDatasetItems(v) { return Array.isArray(v) && v.every(i => i && typeof i === 'object'); } Try / catch
try {
const items = await runActor(actorId, input, { token, timeoutMs: 60000 });
} catch (e) {
if (String(e.message).includes('non-array dataset payload')) {
// inspect raw API response / re-run actor
} else throw e;
} Prevention
- Verify the actor's default dataset contains items before relying on fetchDatasetItems.
- Keep APIFY_TOKEN valid to avoid error bodies being parsed as payloads.
- Pin/monitor Apify API changes; unwrap nested envelopes defensively.
- Log raw payloads in development to catch shape changes early.
When it happens
Trigger: Calling runActor/fetchDatasetItems when the Apify API returns a non-array dataset payload — e.g. an error object, null, or an object with items nested elsewhere (API schema change, run whose default dataset is empty and returned as null, or an auth/rate-limit error body parsed as JSON).
Common situations: Apify API changes the dataset endpoint response envelope; actor runs that produce no dataset; expired/invalid token causing an error JSON body instead of an items array; network proxy returning JSON error payloads.
Related errors
- Apify actor finished with status
- Apify did not return a run id
- 4dayweek: unexpected API response on page
- a16z-speedrun-talent: unexpected API response on page
- agentic-jobs: parsed 0 jobs from the API — the response…
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/b2729b4610d17ea1.
Report an issue: GitHub.
Appendix: 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 aac998c7ed)