santifer/career-ops · error

greenhouse: invalid URL

Error message

greenhouse: invalid URL: ${url}

What it means

assertGreenhouseUrl validates every Greenhouse URL before any network request is made. If the string cannot be parsed by the URL constructor (empty, malformed, missing scheme, illegal characters), the provider throws this error immediately. It is a fail-fast input validation so the scan never hits the network with a broken URL.

Solutions

  1. Print the offending URL from the error message and fix the entry in portals.yml (usually add the https:// scheme).
  2. Validate config at startup with new URL() on every api:/careers_url field before scanning.
  3. Check for shell/env expansion issues that left the variable empty.
  4. If the URL is built programmatically, ensure all parts are joined with proper separators and no whitespace.

Example fix

// before (portals.yml)
api: boards-api.greenhouse.io/acme
// after
api: https://boards-api.greenhouse.io/acme
Defensive patterns

Strategy: validation

Validate before calling

function isValidGreenhouseUrl(url) {
  try { new URL(url); return true; } catch { return false; }
}
if (!isValidGreenhouseUrl(entry.api)) throw new Error(`bad greenhouse api url: ${entry.api}`);

Type guard

const isParseableUrl = (s) => { try { return Boolean(new URL(s)); } catch { return false; } };

Try / catch

try {
  await provider.fetch(entry, ctx);
} catch (err) {
  if (/greenhouse: invalid URL/.test(err.message)) {
    console.error(`Fix portals.yml entry "${entry.name}": ${err.message}`);
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling assertGreenhouseUrl (via resolveApiUrl or fetch) with a value that new URL() rejects: an empty string, a board token with spaces, a careers_url like 'greenhouse.io/acme' without a scheme, or a truncated URL from a config typo.

Common situations: portals.yml entries with a bad api:/careers_url value, hand-edited config missing 'https://', environment-variable interpolation producing an empty string, or copy-paste that dropped part of the URL.

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/ed0b2c96fa616552. Report an issue: GitHub.

Appendix: source

Thrown at providers/greenhouse.mjs:26

// country_eligibility filter and visa_filter all read that field, and without
// it every Greenhouse board passed those filters blind.

import { htmlToText } from './_html-to-text.mjs';

const ALLOWED_GREENHOUSE_HOSTS = new Set([
  'boards-api.greenhouse.io',
  'boards.greenhouse.io',
  'job-boards.greenhouse.io',
  'job-boards.eu.greenhouse.io',
]);

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

/** @param {import('./_types.js').PortalEntry} entry */
function resolveApiUrl(entry) {
  if (entry.api) {
    assertGreenhouseUrl(entry.api);
    return entry.api;
  }
  const url = entry.careers_url || '';
  const match = url.match(/job-boards(?:\.eu)?\.greenhouse\.io\/([^/?#]+)/);
  if (match) return `https://boards-api.greenhouse.io/v1/boards/${match[1]}/jobs`;
  return null;
}

View on GitHub (pinned to aac998c7ed)