santifer/career-ops · error · Error
Refusing non-HTTP(S) URL
Error message
Refusing non-HTTP(S) URL: ${url} What it means
The second gate in assertSafeRemoteUrl: the string parses as a URL but its protocol is neither https: nor http:. The library only fetches web pages over HTTP(S), so schemes like file:, ftp:, data:, or javascript: are rejected outright as part of SSRF fail-closed hardening.
Solutions
- Replace the entry with an https:// URL pointing at the live posting
- If you have a local copy of the JD, don't fetch it as a URL — save it as a jds/ capture (e.g. via archive-posting.mjs) and reference local:jds/{file}
- Correct typos in the scheme (http// → https://) so the URL parses with a valid protocol
- Remove ftp:/data:/javascript: style entries from portals.yml / pipeline.md entirely
Example fix
// before (pipeline.md / portals.yml) url: file:///tmp/job-posting.html // after url: https://jobs.acme.com/postings/12345
Defensive patterns
Strategy: validation
Validate before calling
function isWebUrl(s) {
try { const u = new URL(s); return u.protocol === 'https:' || u.protocol === 'http:'; }
catch { return false; }
}
// reject file://, ftp://, data:, etc. before calling fetchJobPage Type guard
function hasHttpScheme(v) {
if (typeof v !== 'string') return false;
const m = v.match(/^https?:\/\//i);
return m !== null;
} Try / catch
try {
const text = await fetchJobPage(url);
} catch (e) {
if (e.message.startsWith('Refusing non-HTTP(S) URL')) {
console.error(`Unsupported scheme for ${url} — use an https:// link or a local jds/ capture`);
return null;
}
throw e;
} Prevention
- Store local JD copies as jds/ captures referenced via local:jds/{file}, not as file:// URLs
- Correct scheme typos immediately (http// → https://) when editing pipeline entries
- Sanitize links pasted from desktop apps that emit custom schemes (webcal:, slack:, etc.)
- Add a config lint that asserts every careers_url entry starts with https://
When it happens
Trigger: fetchJobPage receives a URL whose protocol check fails — e.g. 'file:///home/user/job.html', 'ftp://example.com/posting', 'data:text/html,...', or a config entry like 'localhost.job' with a typo'd scheme such as 'https//example.com' (which parses with protocol 'https:/' malformed... actually parses as scheme 'https' with invalid rest) or 'webcal://...'.
Common situations: Someone pointed a pipeline entry at a locally saved HTML file with file://, an internal tool emitted a custom-scheme deep link, or a copy-paste from a desktop app produced a non-web scheme.
Understand the failure class
Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.
Related errors
- arbeitnow: untrusted hostname
- arbeitnow: URL must use HTTPS
- ashby: untrusted hostname
- bamboohr: untrusted hostname
- breezy: untrusted hostname
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/9f2add13c3b38222.
Report an issue: GitHub.
Appendix: source
Thrown at openrouter-runner.mjs:411
'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 aac998c7ed)