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

  1. Inspect the exact URL in the error message and correct its scheme to http:// or https://.
  2. Find the source entry in data/pipeline.md or portals.yml and fix the offending api/careers_url value.
  3. If you genuinely need a non-HTTP source, do not route it through fetchJobPage — read it locally instead.
  4. 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

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


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