santifer/career-ops · error

ashby: invalid URL

Error message

ashby: invalid URL: ${url}

What it means

The Ashby provider validates every board URL before use. assertAshbyUrl first parses the string with the URL constructor; if parsing fails (malformed URL, empty string, whitespace, missing scheme), it throws this error. It is the first gate in a chain of checks (parseability, HTTPS, hostname allowlist).

Solutions

  1. Print/inspect the exact offending URL value; check for missing `https://` scheme, stray whitespace, or unexpanded placeholders.
  2. Trim and normalize the input: `url.trim()` and prepend `https://` only if an absolute host is present but no scheme.
  3. Fix the offending portals.yml entry so the api/careers URL is a full absolute HTTPS URL.
  4. If building URLs programmatically, use `new URL(path, base)` with a validated base instead of string concatenation.

Example fix

// before
const url = 'jobs.ashby.co/exampleco/postings';
assertAshbyUrl(url); // throws

// after
const url = 'https://jobs.ashby.co/exampleco/postings';
assertAshbyUrl(url); // passes scheme check
Defensive patterns

Strategy: validation

Validate before calling

function isValidAshbyUrl(url) {
  if (typeof url !== 'string' || !url.trim()) return false;
  try { new URL(url.trim()); return true; } catch { return false; }
}

Type guard

function isParsedUrl(value) {
  try { return { ok: true, url: new URL(value) }; } catch { return { ok: false }; }
}

Try / catch

try {
  assertAshbyUrl(entry.api);
} catch (err) {
  console.warn(`Skipping entry ${entry.name}: ${err.message}`);
  return null;
}

Prevention

When it happens

Trigger: Calling assertAshbyUrl (directly or via the provider's fetch path) with a string that `new URL()` cannot parse — e.g. `jobs.ashby.co/board` without scheme, an empty string, a URL containing stray spaces or control characters, or a portals.yml entry whose api/careers URL is malformed.

Common situations: Hand-edited portals.yml entries with typos, URLs pasted with leading/trailing whitespace or quotes, relative URLs taken from a page instead of absolute ones, or template placeholders like `${TENANT}` left unexpanded.

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


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

Appendix: source

Thrown at providers/ashby.mjs:89

  // Ensure correct ordering (min <= max)
  const resolvedMin = /** @type {number} */ (min ?? max);
  const resolvedMax = /** @type {number} */ (max ?? min);
  return {
    min: Math.min(resolvedMin, resolvedMax),
    max: Math.max(resolvedMin, resolvedMax),
    currency: currency.toUpperCase(),
  };
}

const ALLOWED_ASHBY_HOSTS = new Set(['api.ashbyhq.com']);

/** @param {string} url */
function assertAshbyUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`ashby: invalid URL: ${url}`);
  }
  if (parsed.protocol !== 'https:') throw new Error(`ashby: URL must use HTTPS: ${url}`);
  if (!ALLOWED_ASHBY_HOSTS.has(parsed.hostname))
    throw new Error(`ashby: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_ASHBY_HOSTS].join(', ')}`);
  return url;
}

/** @param {import('./_types.js').PortalEntry} entry */
function resolveApiUrl(entry) {
  // Explicit api: wins — lets an entry keep a human-facing corporate
  // careers_url (e.g. https://openai.com/careers) while still pinning the
  // Ashby posting-api board (mirrors greenhouse's api: precedence).
  if (entry.api) {
    assertAshbyUrl(entry.api);
    return entry.api;
  }
  const url = entry.careers_url || '';
  const match = url.match(/jobs\.ashbyhq\.com\/([^/?#]+)/);

View on GitHub (pinned to aac998c7ed)