santifer/career-ops · error · Error

apify: invalid actorId ${JSON.stringify(actorId)}. Expected

Error message

apify: invalid actorId ${JSON.stringify(actorId)}. Expected "owner/actor" or "owner~actor" with letters, digits, "_", ".", or "-" only.

What it means

Thrown by `normalizeActorId` (plugins/apify/_apify.mjs:39) when the actorId does not match the strict regex `^[A-Za-z0-9][A-Za-z0-9_.-]*[~/][A-Za-z0-9][A-Za-z0-9_.-]*$`. Apify actor IDs use the `owner/actor` or `owner~actor` form; strict validation prevents a malformed config from injecting extra `/`, `..`, `?`, or `#` that could escape the intended `/acts/<actor>/runs` path and send the bearer token to an unintended endpoint on api.apify.com. This is a security guard, not just input hygiene.

Source

Thrown at plugins/apify/_apify.mjs:39

const DEFAULT_RUN_TIMEOUT_MS = 180_000;
const POLL_INTERVAL_MS = 3_000;
const PER_REQUEST_TIMEOUT_MS = 15_000;
const CONNECT_RETRY_ATTEMPTS = 3;
const TERMINAL_STATUSES = new Set(['SUCCEEDED', 'FAILED', 'ABORTED', 'TIMED-OUT']);

export function hasToken(token = process.env.APIFY_TOKEN) {
  return Boolean(token);
}

// Apify accepts both "user/actor" and "user~actor" in URLs; normalize to `~`.
// Validate strictly so a malformed config can't escape the intended
// /acts/<actor>/runs path with extra `/`, `..`, `?`, or `#` characters and
// send our bearer token to an unintended endpoint on api.apify.com.
const ACTOR_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]*[~/][A-Za-z0-9][A-Za-z0-9_.-]*$/;

export function normalizeActorId(actorId) {
  if (typeof actorId !== 'string' || !ACTOR_ID_RE.test(actorId)) {
    throw new Error(
      `apify: invalid actorId ${JSON.stringify(actorId)}. ` +
      `Expected "owner/actor" or "owner~actor" with letters, digits, "_", ".", or "-" only.`
    );
  }
  const [owner, name] = actorId.split(/[~/]/, 2);
  return `${encodeURIComponent(owner)}~${encodeURIComponent(name)}`;
}

// Apify supports auth via ?token= or Authorization: Bearer. The query-string
// form leaks the token into HTTP access logs and any error/log line that
// includes the URL, so always use the header.
function authHeaders(token) {
  return { authorization: `Bearer ${token}` };
}

function sleep(ms) {
  return new Promise(r => setTimeout(r, ms));
}

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Use the canonical `owner/actor` or `owner~actor` form, e.g. `misceres/indeed-scraper`.
  2. Strip any leading/trailing whitespace, slashes, or query strings before passing the actorId.
  3. Copy the actor ID directly from the Apify store URL path (the segment after /acts/).
  4. If you only have a URL like https://apify.com/store/acts/misceres/indeed-scraper, take the `misceres/indeed-scraper` portion.

Example fix

# before — portals.yml
- name: indeed
  provider: apify
  actor: indeed-scraper          # missing owner → invalid
# after
- name: indeed
  provider: apify
  actor: misceres/indeed-scraper
Defensive patterns

Strategy: validation

Validate before calling

import { normalizeActorId } from './plugins/apify/_apify.mjs';
// Validate the actorId shape before persisting it in portals.yml.
function isValidActorId(id) {
  try { normalizeActorId(id); return true; }
  catch { return false; }
}
for (const e of portals.filter(p => p.provider === 'apify')) {
  if (!isValidActorId(e.actor)) throw new Error(`Entry '${e.name}' has a malformed apify actor: '${e.actor}'`);
}

Type guard

const ACTOR_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]*[~/][A-Za-z0-9][A-Za-z0-9_.-]*$/;
/** @param {unknown} v */
function isValidActorId(v) {
  return typeof v === 'string' && ACTOR_ID_RE.test(v);
}

Try / catch

try {
  await runActor(entry.actor, entry.input, opts);
} catch (err) {
  if (/invalid actorId/.test(err.message)) {
    console.error(`Fix the actor id in portals.yml: ${err.message}`);
    process.exitCode = 2;
  } else throw err;
}

Prevention

When it happens

Trigger: `runActor(actorId, ...)` or `startRun` is called with an actorId that is not a string, missing the owner/actor separator, contains disallowed characters (slash variants, spaces, path traversal), or has an empty owner/actor segment. The regex is tested before any URL is built.

Common situations: portals.yml entry has `actor: indeed-scraper` (missing owner); `actor: misceres/indeed-scraper/extra` (extra slash); `actor: ../admin`; a copy-paste that included a trailing slash or query string; passing a full Apify URL instead of the actor ID.

Related errors


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