santifer/career-ops · error · Error
Invalid URL: ${url}
Error message
Invalid URL: ${url} What it means
Thrown by assertSafeRemoteUrl() in openrouter-runner.mjs when new URL(url) throws — i.e. the string is not a parseable URL (missing protocol, illegal characters, malformed scheme). This is the first guard in the SSRF defense-in-depth chain (before the protocol and private-host checks), failing closed on any URL the platform cannot parse. URLs here come from the user's portals.yml / pipeline.md job links.
Source
Thrown at openrouter-runner.mjs:386
ctx.profile,
'---',
'CV (Markdown):',
ctx.cv,
'---',
'OUTPUT LANGUAGE:',
languageInstruction,
].filter(Boolean).join('\n\n');
}
// ---------------------------------------------------------------------------
// Job page content fetcher (Playwright-first, plain fetch fallback)
// ---------------------------------------------------------------------------
// Reject unsafe fetch targets (SSRF defense-in-depth): http(s) only, never
// loopback / link-local / private / cloud-metadata hosts. URLs come from the
// user's own portals.yml / pipeline.md, but we still fail closed.
function assertSafeRemoteUrl(url) {
let u;
try { u = new URL(url); } catch { throw new Error(`Invalid URL: ${url}`); }
if (u.protocol !== 'https:' && u.protocol !== 'http:') {
throw new Error(`Refusing non-HTTP(S) URL: ${url}`);
}
const host = u.hostname.toLowerCase();
const blocked = host === 'localhost' || host === '::1' || host.endsWith('.local') ||
/^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host) ||
/^169\.254\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host);
if (blocked) throw new Error(`Refusing private/loopback host: ${host}`);
return u;
}
async function fetchJobPage(url) {
assertSafeRemoteUrl(url);
let chromium;
try {
({ chromium } = await import('playwright'));
} catch {
console.warn('[fetch] Playwright unavailable — falling back to plain fetch.');View on GitHub (pinned to 9b17a8ac97)
Solutions
- Ensure the URL has an explicit scheme: prepend 'https://' if missing — 'https://company.com/jobs/123'.
- Validate the URL field in portals.yml/pipeline.md is complete and not truncated.
- Strip stray whitespace/control characters before passing.
- If the field can be empty, guard for emptiness before calling assertSafeRemoteUrl.
Example fix
// before
assertSafeRemoteUrl('company.com/jobs/123');
// throws: Invalid URL: company.com/jobs/123
// after
assertSafeRemoteUrl('https://company.com/jobs/123');
// or normalize first
const u = raw.startsWith('http') ? raw : `https://${raw}`; Defensive patterns
Strategy: validation
Validate before calling
function normalizeUrl(raw) {
if (!raw || typeof raw !== 'string') return null;
const s = raw.trim();
if (!s) return null;
const withScheme = /^https?:\/\//i.test(s) ? s : `https://${s}`;
try { return new URL(withScheme); } catch { return null; }
}
const u = normalizeUrl(input);
if (!u) skip('Invalid URL — cannot parse'); Type guard
/** True if the value parses as a URL new URL() accepts. */
function isParseableUrl(v) {
if (!v || typeof v !== 'string') return false;
try { new URL(v); return true; } catch { return false; }
} Try / catch
try {
assertSafeRemoteUrl(url);
} catch (e) {
if (e.message.startsWith('Invalid URL:')) {
// normalize: prepend scheme, then retry once
const fixed = /^https?:\/\//i.test(url) ? url : `https://${url}`;
assertSafeRemoteUrl(fixed);
} else throw e;
} Prevention
- Always store full https:// URLs in portals.yml/pipeline.md.
- Run a URL-format lint over portal entries rejecting schemeless values.
- Normalize schemeless inputs by prepending https:// before calling assertSafeRemoteUrl.
- Guard empty/whitespace URL fields upstream.
When it happens
Trigger: Passing a string that is not a valid URL: 'company.com/jobs' (no scheme), 'ftp://...' (handled by the next guard, not this one — this is purely parse failure), strings with spaces/control chars, bare paths like '/jobs/123', or undefined/null coerced to string.
Common situations: Pipeline entry missing the https:// scheme; a copy-paste that dropped the protocol; a malformed portal URL in portals.yml; an empty or whitespace URL field; a JD capture whose URL got truncated.
Related errors
- DNS resolution returned no addresses for ${hostname}
- Access denied: Egress guard blocked private target IP ${ip}
- Invalid page budget "${maxPages}". Use a positive integer.
- CV is ${pageCount} ${actualLabel}; the allowed maximum is ${
- [models] Failed to fetch free model list: ${reason}. Check t
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/b6ead8c3bc3192ad.
Report an issue: GitHub.