santifer/career-ops · error · Error
Refusing non-HTTP(S) URL: ${url}
Error message
Refusing non-HTTP(S) URL: ${url} What it means
Thrown by assertSafeRemoteUrl() in openrouter-runner.mjs when fetchJobPage() is asked to retrieve a URL whose protocol is neither http: nor https: (e.g. file:, javascript:, data:, ftp:). It is an SSRF defense-in-depth guard: even though URLs come from the user's own portals.yml / pipeline.md, the runner fails closed against any non-HTTP(S) scheme before passing it to Playwright or fetch().
Source
Thrown at openrouter-runner.mjs:388
'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
- Inspect the exact URL in the error message and correct its scheme to http:// or https://.
- Find the source entry in data/pipeline.md or portals.yml and fix the offending api/careers_url value.
- If you genuinely need a non-HTTP source, do not route it through fetchJobPage — read it locally instead.
- Add a unit test feeding the corrected URL through assertSafeRemoteUrl() to confirm it passes.
Example fix
// before url: "file:///home/me/jd.html" // after url: "https://careers.example.com/jobs/123"
Defensive patterns
Strategy: validation
Validate before calling
function isHttpUrl(s) {
let u;
try { u = new URL(s); } catch { return false; }
return u.protocol === 'http:' || u.protocol === 'https:';
}
// before calling fetchJobPage:
if (!isHttpUrl(url)) throw new Error(`URL must be http(s): ${url}`); Type guard
/** Narrows a string to a valid http/https URL. */
function isHttpUrl(s) {
if (typeof s !== 'string') return false;
let u;
try { u = new URL(s); } catch { return false; }
return u.protocol === 'http:' || u.protocol === 'https:';
} Prevention
- Validate URLs at the portals.yml/pipeline.md ingest boundary, not just at fetch time.
- Use a linter/schema check that rejects non-http(s) entries in config.
- Never pass user/clipboard text straight to fetchJobPage without a URL parse.
When it happens
Trigger: A pipeline.md or portals.yml entry contains a URL with a non-HTTP(S) protocol; calling fetchJobPage('file:///etc/passwd') or fetchJobPage('javascript:alert(1)'); a malformed/copy-pasted URL where the scheme got stripped or replaced.
Common situations: A portals.yml api/careers_url value was pasted from an email/tooltip that prefixed it with a non-http scheme; a relative-looking URL was accidentally prefixed with file:; data: URIs used in a test fixture; a typo inserted a stray character before the scheme.
Related errors
- Refusing private/loopback host: ${host}
- Access denied: Egress guard blocked private target IP ${ip}
- plugin egress to ${hostname} is blocked (private/loopback/me
- plugin egress: ${hostname} resolves to a blocked address (${
- larajobs: untrusted hostname "${parsed.hostname}" - must be
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/9f2add13c3b38222.
Report an issue: GitHub.