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
- Use the canonical `owner/actor` or `owner~actor` form, e.g. `misceres/indeed-scraper`.
- Strip any leading/trailing whitespace, slashes, or query strings before passing the actorId.
- Copy the actor ID directly from the Apify store URL path (the segment after /acts/).
- 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
- Lint apify `actor` fields in portals.yml with the same regex in CI.
- Copy actor IDs verbatim from the Apify store URL path.
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
- apify: invalid timeoutMs ${JSON.stringify(timeoutMs)} (must
- apify: entry ${entry.name} missing 'actor' (e.g. misceres/in
- apify: entry ${entry.name} has invalid field_map. Each of ti
- Unsupported profile photo data URL (expected base64 PNG, JPE
- Unsupported profile photo URL scheme: ${photo.split(':', 1)[
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/7ae355a3a5002fdd.
Report an issue: GitHub.