santifer/career-ops · error · Error

lever: invalid URL: ${url}

Error message

lever: invalid URL: ${url}

What it means

assertLeverUrl mirrors the Larajobs pattern: it first attempts new URL(url). If the constructor throws — the string is not an absolute parseable URL — this error fires. It guards the Lever postings fetch and the api: auto-detection path.

Source

Thrown at providers/lever.mjs:16

// @ts-check
/** @typedef {import('./_types.js').Provider} Provider */

// Lever provider — hits the public postings endpoint.
// Auto-detects from careers_url via jobs.(eu.)?lever.co/<slug>.
// Handles both explicit `api:` URLs and auto-detection from `careers_url`.

const ALLOWED_LEVER_HOSTS = new Set(['api.lever.co', 'api.eu.lever.co']);

/** @param {string} url */
function assertLeverUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`lever: invalid URL: ${url}`);
  }
  if (parsed.protocol !== 'https:') throw new Error(`lever: URL must use HTTPS: ${url}`);
  if (!ALLOWED_LEVER_HOSTS.has(parsed.hostname))
    throw new Error(`lever: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_LEVER_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://www.coalfire.com/careers) while still pinning
  // the Lever postings board (mirrors greenhouse's api: precedence).
  if (entry.api) {
    assertLeverUrl(entry.api);
    return entry.api;
  }
  let url;
  try {

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Read the value in the message — it is the exact unparseable string.
  2. Set api: to a full URL such as https://api.lever.co/v0/postings/<slug>.
  3. Strip whitespace/newlines from the config value.
  4. If relying on auto-detection from careers_url, ensure careers_url is a full https URL containing lever.co.

Example fix

// before (portals.yml)
api: lever.co/postings/acme

// after
api: https://api.lever.co/v0/postings/acme
Defensive patterns

Strategy: validation

Validate before calling

import { URL } from 'node:url';
export function isValidAbsoluteUrl(value) {
  if (typeof value !== 'string' || !value) return false;
  try { new URL(value); return true; } catch { return false; }
}
// Pre-validate before the provider runs:
// if (!isValidAbsoluteUrl(entry.api)) failConfig(`lever api invalid for ${entry.name}`);

Type guard

/** @param {string} url */
function isParseableUrl(url) {
  try { new URL(url); return true; } catch { return false; }
}

Try / catch

try {
  assertLeverUrl(entry.api);
} catch (err) {
  console.warn(`lever entry ${entry.name} has bad api URL: ${err.message}`);
  entry.disabled = true;
}

Prevention

When it happens

Trigger: entry.api or a careers_url-derived value passed to assertLeverUrl is empty, relative, missing a scheme, or otherwise rejected by the URL constructor. resolveApiUrl calls assertLeverUrl on entry.api when present, and on the derived api.lever.co URL.

Common situations: portals.yml api: field is blank or missing https://; a slug-based derivation produced an incomplete URL; whitespace/control characters were pasted into the config.

Related errors


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