santifer/career-ops · error

eightfold: invalid URL

Error message

eightfold: invalid URL: ${url}

What it means

assertEightfoldUrl() is the provider's SSRF guard: every request URL must parse as a URL, use HTTPS, and have a hostname matching *.eightfold.ai before any fetch happens. This specific throw fires when `new URL(url)` itself throws — the input is not a syntactically valid absolute URL (missing scheme, spaces, empty string, etc.). It is a fail-fast validation so a malformed configured URL never reaches the network layer.

Solutions

  1. Add the scheme to the configured URL: use https://<tenant>.eightfold.ai/careers, not a bare hostname.
  2. Print/inspect the actual offending value from the error message — it is interpolated verbatim, so an empty string means an unset config field.
  3. Trim whitespace and re-check for typos or unexpanded placeholders in the portals.yml entry.
  4. Validate the URL with `new URL(value)` in a quick Node snippet before putting it into config.

Example fix

// before (portals.yml)
- name: bayer
  api: bayer.eightfold.ai/careers
// after
- name: bayer
  api: https://bayer.eightfold.ai/careers
Defensive patterns

Strategy: validation

Validate before calling

function assertUsableEightfoldUrl(url) {
  if (typeof url !== 'string' || !url.trim()) throw new Error('eightfold URL is empty/missing');
  const u = new URL(url); // throws SyntaxError on malformed input, same condition as the provider
  return u;
}
assertUsableEightfoldUrl(entry.api || entry.careers_url);

Type guard

function isAbsoluteHttpUrl(v) {
  if (typeof v !== 'string') return false;
  try { new URL(v); return true; } catch { return false; }
}

Try / catch

try {
  await provider.fetch(entry, ctx);
} catch (e) {
  if (e.message.startsWith('eightfold: invalid URL:')) {
    // value was not parseable — log the exact configured value and stop, don't retry
    console.error(`Bad url in entry ${entry.name}: ${JSON.stringify(e.message)}`);
  } else throw e;
}

Prevention

When it happens

Trigger: assertEightfoldUrl is called with a value that cannot be parsed by the WHATWG URL constructor: an empty string, a bare tenant name like 'bayer.eightfold.ai' with no scheme, a URL with invalid characters, or a config value that is undefined/null coerced into the call path.

Common situations: Portals.yml entry with `api:` set to 'bayer.eightfold.ai/careers' (missing https://); an env-var or CLI substitution that expanded to an empty string; a copy-pasted URL containing stray whitespace or a newline; a template placeholder like {tenant} that was never filled in.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/a21261888fc2e568. Report an issue: GitHub.

Appendix: source

Thrown at providers/eightfold.mjs:69

const MAX_PAGES_CAP = 1000;
// Same-host pacing between pages inside one tenant's own pagination loop.
// Eightfold's edge rate-limits bursts, and a 616-job board is 62 requests.
const INTER_PAGE_DELAY_MS = 250;

const RETRY_POLICY = { retries: 3, baseDelayMs: 500, maxDelayMs: 8_000 };

/**
 * SSRF guard — every request URL passes through here before it is fetched.
 *
 * @param {string} url
 * @returns {string} the same URL, when it is a trusted Eightfold endpoint.
 */
function assertEightfoldUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`eightfold: invalid URL: ${url}`);
  }
  if (parsed.protocol !== 'https:') throw new Error(`eightfold: URL must use HTTPS: ${url}`);
  if (!EIGHTFOLD_HOST_RE.test(parsed.hostname)) {
    throw new Error(`eightfold: untrusted hostname "${parsed.hostname}" — must match *.eightfold.ai`);
  }
  return url;
}

/** @param {number} ms @param {any} ctx */
function sleep(ms, ctx) {
  if (typeof ctx?.sleep === 'function') return ctx.sleep(ms);
  return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
 * Eightfold reports timestamps as epoch SECONDS (`t_create`, `t_update`), not
 * the ISO strings every other provider gets. Converted here; anything
 * non-finite or non-positive is dropped rather than guessed at.

View on GitHub (pinned to aac998c7ed)